javascript
02 | Spring Data Common 之 Repository 如何全面掌握?
通過(guò)上一課時(shí),我們知道了 Spring Data 對(duì)整個(gè)數(shù)據(jù)操作做了很好的封裝,其中 Spring Data Common 定義了很多公用的接口和一些相對(duì)數(shù)據(jù)操作的公共實(shí)現(xiàn)(如分頁(yè)排序、結(jié)果映射、Autiting 信息、事務(wù)等),而 Spring Data JPA 就是 Spring Data Common 的關(guān)系數(shù)據(jù)庫(kù)的查詢(xún)實(shí)現(xiàn)。
所以本課時(shí)我們來(lái)了解一下 Spring Data Common 的核心內(nèi)容——Repository。我將從 Repository 的所有子類(lèi)著手,帶領(lǐng)你逐步掌握 CrudRepository、PageingAndSortingRepository、JpaRepository的使用。
在講解 Repository 之前,我們先來(lái)看看 Spring Data JPA 所依賴(lài)的 jar 包關(guān)系是什么樣的,看下 Spring Data Common 的 jar 依賴(lài)關(guān)系。
Spring Data Common 的依賴(lài)關(guān)系
我們通過(guò) Gradle 看一下項(xiàng)目依賴(lài),了解一下 Spring Data Common 的依賴(lài)關(guān)系。
Resposiory 是 Spring Data 里面進(jìn)行數(shù)據(jù)庫(kù)操作頂級(jí)的抽象接口,里面什么方法都沒(méi)有,但是如果任何接口繼承它,就能得到一個(gè) Repository,還可以實(shí)現(xiàn) JPA 的一些默認(rèn)實(shí)現(xiàn)方法。Spring 利用 Respository 作為 DAO 操作的 Type,以及利用 Java 動(dòng)態(tài)代理機(jī)制就可以實(shí)現(xiàn)很多功能,比如為什么接口就能實(shí)現(xiàn) DB 的相關(guān)操作?這就是 Spring 框架的高明之處。
Spring 在做動(dòng)態(tài)代理的時(shí)候,只要是它的子類(lèi)或者實(shí)現(xiàn)類(lèi),再利用 T 類(lèi)以及 T 類(lèi)的 主鍵 ID 類(lèi)型作為泛型的類(lèi)型參數(shù),就可以來(lái)標(biāo)記出來(lái)、并捕獲到要使用的實(shí)體類(lèi)型,就能幫助使用者進(jìn)行數(shù)據(jù)庫(kù)操作。
Repository 類(lèi)層次關(guān)系
下面我們來(lái)根據(jù)存這個(gè)基類(lèi) Repository 接口,順藤摸瓜看看 Spring Data JPA 里面都有什么。
首先,我們用工具 Intellij Idea,打開(kāi)類(lèi) Repository.class,然后依次導(dǎo)航 → Hierchy 類(lèi)型,會(huì)得到如下圖所示的結(jié)果:
通過(guò)該層次結(jié)構(gòu)視圖,你就會(huì)明白基類(lèi) Repository 的用意,由此可知,存儲(chǔ)庫(kù)分為以下 4 個(gè)大類(lèi)。
-
ReactiveCrudRepository 這條線(xiàn)是響應(yīng)式編程,主要支持當(dāng)前 NoSQL 方面的操作,因?yàn)檫@方面大部分操作都是分布式的,所以由此我們可以看出 Spring Data 想統(tǒng)一數(shù)據(jù)操作的“野心”,即想提供關(guān)于所有 Data 方面的操作。目前 Reactive 主要有 Cassandra、MongoDB、Redis 的實(shí)現(xiàn)。
-
RxJava2CrudRepository 這條線(xiàn)是為了支持 RxJava 2 做的標(biāo)準(zhǔn)響應(yīng)式編程的接口。
-
CoroutineCrudRepository 這條繼承關(guān)系鏈?zhǔn)菫榱酥С?Kotlin 語(yǔ)法而實(shí)現(xiàn)的。
-
CrudRepository 這條繼承關(guān)系鏈正是本課時(shí)我要詳細(xì)介紹的 JPA 相關(guān)的操作接口,你也可以把我的這種方法應(yīng)用到另外 3 種繼承關(guān)系鏈里面學(xué)習(xí)。
然后,通過(guò) Intellij Idea,我們也可以打開(kāi)類(lèi) UserRepository.java(第一課時(shí)“Spring Data JPA 初識(shí)”里面的案例),在此類(lèi)里面,鼠標(biāo)右鍵點(diǎn)擊 Show Diagram 顯示層次結(jié)構(gòu)圖,用圖表的方式查看類(lèi)的關(guān)系層次,打開(kāi)后如下圖(Repository 繼承關(guān)系圖)所示:
在這里簡(jiǎn)單介紹一下,我們需要掌握和使用到的類(lèi)如下所示。
7 個(gè)大 Repository 接口:
-
Repository(org.springframework.data.repository),沒(méi)有暴露任何方法;
-
CrudRepository(org.springframework.data.repository),簡(jiǎn)單的 Curd 方法;
-
PagingAndSortingRepository(org.springframework.data.repository),帶分頁(yè)和排序的方法;
-
QueryByExampleExecutor(org.springframework.data.repository.query),簡(jiǎn)單 Example 查詢(xún);
-
JpaRepository(org.springframework.data.jpa.repository),JPA 的擴(kuò)展方法;
-
JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 擴(kuò)展查詢(xún);
-
QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封裝。
兩大 Repository 實(shí)現(xiàn)類(lèi):
-
SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默認(rèn)實(shí)現(xiàn)類(lèi);
-
QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的實(shí)現(xiàn)類(lèi)。
關(guān)于其他的類(lèi),后面我也會(huì)通過(guò)不同方式的講解,讓你一一認(rèn)識(shí)。下面我們?cè)賮?lái)看一個(gè) Repository 實(shí)例。
import org.springframework.data.repository.Repository;import java.util.List;public interface UserRepository extends Repository<User,Integer> {//根據(jù)名稱(chēng)進(jìn)行查詢(xún)用戶(hù)列表List<User> findByName(String name);// 根據(jù)用戶(hù)的郵箱和名稱(chēng)查詢(xún)List<User> findByEmailAndName(String email, String name);}由于 Repository 接口里面沒(méi)有任何方法,所以此 UserRepository 對(duì)外只有兩個(gè)可用方法,如上面的代碼一樣。Service 里面只能調(diào)用到 findByName 和 findByEmailAndName 兩個(gè)方法,我們通過(guò) IDEA 的 Structure 也可以看到對(duì)外只有兩個(gè)方法可用,如下所示:
這時(shí),我在第 01 課時(shí)中“Spring Boot 和 Spring Data JPA 的 Demo 演示”的例子里,提到過(guò)的 Controller 中引用 userRepository 的 save 和 findAll 方法就會(huì)報(bào)錯(cuò)。
上面這個(gè)實(shí)例通過(guò)繼承 Repository,使 Spring 容器知道 UserRepository 是 DB 操作的類(lèi),是我們可以對(duì) User 對(duì)象進(jìn)行 CURD 的操作。這時(shí)我們對(duì) Repository 有了一定的掌握,接下來(lái)再來(lái)看看它的直接子類(lèi) CurdRepository 接口都為我們提供了哪些方法。
CrudRepository 接口
下面我們通過(guò) IDEA 工具,看下 CrudRepository 為我們提供的方法有哪些。
通過(guò)上圖,你可以看到其中展示的一些方法,在這里一一說(shuō)明一下:
-
count(): long 查詢(xún)總數(shù)返回 long 類(lèi)型;
-
void delete(T entity) 根據(jù) entity 進(jìn)行刪除;
-
void deleteAll(Iterable<? extends T> entities) 批量刪除;
-
void deleteAll() 刪除所有;原理可以通過(guò)剛才的類(lèi)關(guān)系查看,CrudRepository 的實(shí)現(xiàn)方法如下:
通過(guò)源碼我們可以看出 SimpleJpaRepository 里面的 deleteAll 是利用 for 循環(huán)調(diào)用 delete 方法進(jìn)行刪除操作。我們接著看 CrudRepository 提供的方法。
-
void deleteById(ID id); 根據(jù)主鍵刪除,查看源碼會(huì)發(fā)現(xiàn),其是先查詢(xún)出來(lái)再進(jìn)行刪除;
-
boolean existsById(ID id) 根據(jù)主鍵判斷實(shí)體是否存在;
-
Iterable findAllById(Iterable ids); 根據(jù)主鍵列表查詢(xún)實(shí)體列表;
-
Iterable findAll(); 查詢(xún)實(shí)體的所有列表;
-
Optional findById(ID id); 根據(jù)主鍵查詢(xún)實(shí)體,返回 JDK 1.8 的 Optional,這可以避免 null exception;
-
<‘S’ extends T> S save(S entity); 保存實(shí)體方法,參數(shù)和返回結(jié)果可以是實(shí)體的子類(lèi);
-
saveAll(Iterable entities) : 批量保存,原理和 save方法相同,我們?nèi)タ磳?shí)現(xiàn)的話(huà),就是 for 循環(huán)調(diào)用上面的 save 方法。
上面這些方法是 CrudRepository 對(duì)外暴露的常見(jiàn)的 Crud 接口,我們?cè)趯?duì)數(shù)據(jù)庫(kù)進(jìn)行 Crud 的時(shí)候就會(huì)運(yùn)用到,如我們打算對(duì) User 實(shí)體進(jìn)行 Curd 操作,來(lái)看一下應(yīng)該怎么寫(xiě),如下所示:
public interface UserRepository extends CrudRepository<User,Long> { }
這里我們需要注意一下 save 和 deleteById 的實(shí)現(xiàn)邏輯,分別看看一下這兩種方法是怎么實(shí)現(xiàn)的:
你會(huì)發(fā)現(xiàn)在進(jìn)行 Update、Delete、Insert 等操作之前,我們看上面的源碼,會(huì)通過(guò) findById 先查詢(xún)一下實(shí)體對(duì)象的 ID,然后再去對(duì)查詢(xún)出來(lái)的實(shí)體對(duì)象進(jìn)行保存操作。而如果在 Delete 的時(shí)候,查詢(xún)到的對(duì)象不存在,則直接拋異常。
我在這里特別強(qiáng)調(diào)了一下 Delete 和 Save 方法,是因?yàn)樵趯?shí)際工作中,看到有的同事畫(huà)蛇添足:自己在做 Save 的時(shí)候先去 Find 一下,其實(shí)是沒(méi)有必要的,Spring JPA 底層都考慮到了。這里其實(shí)是想告訴你,當(dāng)我們用任何第三方方法的時(shí)候,最好先查一下其源碼和邏輯或者 API,然后再寫(xiě)出優(yōu)雅的代碼。
關(guān)于 entityInformation.isNew(entity),在這里簡(jiǎn)單說(shuō)一下,如果當(dāng)傳遞的參數(shù)里面沒(méi)有 ID,則直接 insert;若當(dāng)傳遞的參數(shù)里面有 ID,則會(huì)觸發(fā) select 查詢(xún)。此方法會(huì)去看一下數(shù)據(jù)庫(kù)里面是否存在此記錄,若存在,則 update,否則 insert。后面第 14 課時(shí)講樂(lè)觀鎖實(shí)現(xiàn)機(jī)制的時(shí)候會(huì)有詳細(xì)介紹。
PagingAndSortingRepository 接口
上面我們介紹完了 Crud 的基本操作,發(fā)現(xiàn)沒(méi)有分頁(yè)和排序方法,那么接下來(lái)講講 PagingAndSortingRepository 接口,該接口也是 Repository 接口的子類(lèi),主要用于分頁(yè)查詢(xún)和排序查詢(xún)。我們先來(lái)看看 PagingAndSortingRepository 的源碼,了解一下都有哪些方法。
PagingAndSortingRepository 的源碼
PagingAndSortingRepository 源碼發(fā)現(xiàn)有兩個(gè)方法,分別是用于分頁(yè)和排序的時(shí)候使用的,如下所示:
package org.springframework.data.repository;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.data.domain.Sort;@NoRepositoryBeanpublic interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {Iterable<T> findAll(Sort sort); (1)Page<T> findAll(Pageable pageable); (2)}其中,第一個(gè)方法 findAll 參數(shù)是 Sort,是根據(jù)排序參數(shù),實(shí)現(xiàn)不同的排序規(guī)則獲取所有的對(duì)象的集合;第二個(gè)方法 findAll 參數(shù)是 Pageable,是根據(jù)分頁(yè)和排序進(jìn)行查詢(xún),并用 Page 對(duì)返回結(jié)果進(jìn)行封裝。而 Pageable 對(duì)象包含 Page 和 Sort 對(duì)象。
通過(guò)開(kāi)篇講到的【Repository 繼承關(guān)系圖】和上面介紹的一大堆源碼可以看到,PagingAndSortingRepository 繼承了 CrudRepository,進(jìn)而擁有了父類(lèi)的方法,并且增加了分頁(yè)和排序等對(duì)查詢(xún)結(jié)果進(jìn)行限制的通用的方法。
PagingAndSortingRepository 和 CrudRepository 都是 Spring Data Common 的標(biāo)準(zhǔn)接口,那么實(shí)現(xiàn)類(lèi)是什么呢?如果我們采用 JPA,那對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)就是 Spring Data JPA 的 jar 包里面的 SimpleJpaRepository。如果是其他 NoSQL的 實(shí)現(xiàn)如 MongoDB,那實(shí)現(xiàn)就在 Spring Data MongoDB 的 jar 里面的 MongoRepositoryImpl。
關(guān)于 PagingAndSortingRepository 源碼的介紹到這里,下面我們看看怎么使用這兩個(gè)方法。
PagingAndSortingRepository 使用案例
第一步:我們定一個(gè) UserRepository 類(lèi)來(lái)繼承 PagingAndSortingRepository 接口,實(shí)現(xiàn)對(duì) User 的分頁(yè)和排序操作,實(shí)現(xiàn)源碼如下:
package com.example.jpa.example1;import org.springframework.data.repository.PagingAndSortingRepository;public interface UserRepository extends PagingAndSortingRepository<User,Long> {}第二步:我們利用 UserRepository 直接繼承 PagingAndSortingRepository 即可,而 Controller 里面就可以有如下用法了:
/*** 驗(yàn)證排序和分頁(yè)查詢(xún)方法,Pageable的默認(rèn)實(shí)現(xiàn)類(lèi):PageRequest* @return*/@GetMapping(path = "/page")@ResponseBodypublic Page<User> getAllUserByPage() {return userRepository.findAll(PageRequest.of(1, 20,Sort.by(new Sort.Order(Sort.Direction.ASC,"name"))));}/*** 排序查詢(xún)方法,使用Sort對(duì)象* @return*/@GetMapping(path = "/sort")@ResponseBodypublic Iterable<User> getAllUsersWithSort() {return userRepository.findAll(Sort.by(new Sort.Order(Sort.Direction.ASC,"name")));}到這里,你已經(jīng)實(shí)現(xiàn)了對(duì)實(shí)體 User 的 DB 操作,那么以上內(nèi)容我們學(xué)習(xí)了 CURD 和分頁(yè)排序的基本操作,下面看看 JpaRepsitory 的接口為我們提供了哪些方法。
JpaRepository 接口
到這里可以進(jìn)入到分水嶺了,上面的那些都是 Spring Data 為了兼容 NoSQL 而進(jìn)行的一些抽象封裝,而從 JpaRepository 開(kāi)始是對(duì)關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行抽象封裝。從類(lèi)圖可以看出來(lái)它繼承 PagingAndSortingRepository 類(lèi),也就繼承了其所有方法,并且其實(shí)現(xiàn)類(lèi)也是 SimpleJpaRepository。從類(lèi)圖上還可以看出 JpaRepository 繼承和擁有了 QueryByExampleExecutor 的相關(guān)方法,我們先來(lái)看一下 JpaRepository 有哪些方法。一樣的道理,我們直接看它的源碼,看 Structure 即可,如下圖所示:
涉及 QueryByExample 的部分我們?cè)?11 課時(shí)“JpaRepository 如何自定義”再詳細(xì)介紹,而 JpaRepository 里面重點(diǎn)新增了批量刪除,優(yōu)化了批量刪除的性能,類(lèi)似于之前 SQL 的 batch 操作,并不是像上面的 deleteAll 來(lái) for 循環(huán)刪除。其中 flush() 和 saveAndFlush() 提供了手動(dòng)刷新 session,把對(duì)象的值立即更新到數(shù)據(jù)庫(kù)里面的機(jī)制。
我們都知道 JPA 是 由 Hibernate 實(shí)現(xiàn)的,所以有 session 一級(jí)緩存的機(jī)制,當(dāng)調(diào)用 save() 方法的時(shí)候,數(shù)據(jù)庫(kù)里面是不會(huì)立即變化的,其原理我將在 21 課時(shí)“Persistence Context 所表達(dá)的核心概念是什么”再詳細(xì)講解。JpaRepository 的使用方式也一樣,直接繼承 JpaRepository 即可。
我們看一個(gè) Demo,用 UserRepository 直接繼承 JpaRepository,來(lái)實(shí)現(xiàn) JPA 的相關(guān)方法,如下所示:
public interface UserRepository extends JpaRepository<User,Long> {}這樣 controller 里面就可以直接調(diào)用 JpaRepository 及其父接口里面的所有方法了。
那么以上就是我們對(duì) Repository 及其他子接口的使用案例,在應(yīng)用時(shí),你需要注意不同的接口有不同的方法,根據(jù)業(yè)務(wù)場(chǎng)景繼承不同的接口即可。下面我們接著學(xué)習(xí) Repository 的實(shí)現(xiàn)類(lèi) SimpleJpaRepository。
Repository 的實(shí)現(xiàn)類(lèi) SimpleJpaRepository
關(guān)系數(shù)據(jù)庫(kù)的所有 Repository 接口的實(shí)現(xiàn)類(lèi)就是 SimpleJpaRepository,如果有些業(yè)務(wù)場(chǎng)景需要進(jìn)行擴(kuò)展了,可以繼續(xù)繼承此類(lèi),如 QueryDsl 的擴(kuò)展(雖然不推薦使用了,但我們可以參考它的做法,自定義自己的 SimpleJpaRepository),如果能將此類(lèi)里面的實(shí)現(xiàn)方法看透了,基本上 JPA 中的 API 就能掌握大部分內(nèi)容。
我們可以通過(guò) Debug 視圖看一下動(dòng)態(tài)代理過(guò)程,如下面【類(lèi)的繼承關(guān)系圖】所示:
你會(huì)發(fā)現(xiàn) UserRepository 的實(shí)現(xiàn)類(lèi)是 Spring 啟動(dòng)的時(shí)候,利用 Java 動(dòng)態(tài)代理機(jī)制幫我們生成的實(shí)現(xiàn)類(lèi),而真正的實(shí)現(xiàn)類(lèi)就是 SimpleJpaRepository。
通過(guò)上面【類(lèi)的繼承關(guān)系圖】也可以知道 SimpleJpaRepository 是 Repository 接口、CrudRepository 接口、PagingAndSortingRepository 接口、JpaRepository 接口的實(shí)現(xiàn)。其中,SimpleJpaRepository 的部分源碼如下:
@Repository@Transactional(readOnly = true)public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";private final JpaEntityInformation<T, ?> entityInformation;private final EntityManager em;private final PersistenceProvider provider;private @Nullable CrudMethodMetadata metadata;......@Transactionalpublic void deleteAllInBatch() {em.createQuery(getDeleteAllQueryString()).executeUpdate();}......通過(guò)此類(lèi)的源碼,我們可以挺清晰地看出 SimpleJpaRepository 的實(shí)現(xiàn)機(jī)制,是通過(guò) EntityManger 進(jìn)行實(shí)體的操作,而 JpaEntityInforMation 里面存在實(shí)體的相關(guān)信息和 Crud 方法的元數(shù)據(jù)等。
上面我們講到利用 Java 動(dòng)態(tài)代理機(jī)制幫我們生成的實(shí)現(xiàn)類(lèi),那么關(guān)于動(dòng)態(tài)代理的實(shí)現(xiàn),我們可以在 RepositoryFactorySupport 設(shè)置一個(gè)斷點(diǎn),啟動(dòng)的時(shí)候,在我們的斷點(diǎn)處就會(huì)發(fā)現(xiàn) UserRepository 的接口會(huì)被動(dòng)態(tài)代理成 SimpleJapRepository 的實(shí)現(xiàn),如下圖所示:
這里需要注意的是每一個(gè) Repository 的子類(lèi),都會(huì)通過(guò)這里的動(dòng)態(tài)代理生成實(shí)現(xiàn)類(lèi)。
Repository 接口給我的啟發(fā)
在接觸了 Repository 的源碼之后,我在工作中遇到過(guò)一些類(lèi)似需要抽象接口和寫(xiě)動(dòng)態(tài)代理的情況,所以對(duì)于 Repository 的源碼,我受到了一些啟發(fā):
第一,上面的 7 個(gè)大 Repository 接口,我們?cè)谑褂玫臅r(shí)候可以根據(jù)實(shí)際場(chǎng)景,來(lái)繼承不同的接口,從而選擇暴露不同的 Spring Data Common 給我們提供的已有接口。這其實(shí)利用了 Java 語(yǔ)言的 interface 特性,在這里可以好好理解一下 interface 的妙用。
第二,利用源碼也可以很好地理解一下 Spring 中動(dòng)態(tài)代理的作用,可以利用這種思想,在改善 MyBatis 的時(shí)候使用。
總結(jié)
本課時(shí)到這里就結(jié)束了,這一課時(shí)我講解了 Repository 接口、CrudRepository 接口、PagingAndSortingRepository 接口、JpaRepository 接口的用法,通過(guò)源碼我們知道了接口里面的方法有哪些、怎么實(shí)現(xiàn)的,也知道了 Spring 的動(dòng)態(tài)代理機(jī)制是怎么運(yùn)用到 UserRepository 接口的。
通過(guò)這一課時(shí),相信你對(duì) Repository 的基本用法,以及接口暴露的方法和使用方法都有了一定的了解,下節(jié)課我會(huì)講解除了 Repository 的接口里面定義的方法之外,還可以在我們的 UserRepository 里面實(shí)現(xiàn)哪些方法,又會(huì)有哪些動(dòng)態(tài)實(shí)現(xiàn)機(jī)制呢?我們到時(shí)見(jiàn)。
源碼位置
spring-data-jp
總結(jié)
以上是生活随笔為你收集整理的02 | Spring Data Common 之 Repository 如何全面掌握?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 01 | Spring Data JPA
- 下一篇: Module not found: Er