大家好,又見面了。
到這裡呢,已經是本SpringData JPA
系列文件的第三篇了,先來回顧下前面兩篇:
-
在第1篇《Spring Data JPA系列1:JDBC、ORM、JPA、Spring Data JPA,傻傻分不清楚?給你個選擇SpringDataJPA的理由!》中,我們對JPA的整體概念有了全面的瞭解。
-
在第2篇《Spring Data JPA系列2:快速在SpringBoot專案中熟練使用JPA》中也知曉了SpringBoot專案快速整合SpringData JPA以及快速上手使用JPA來進行基本的專案開發的技能。
本篇內容將在上一篇已有的內容基礎上,進一步的聊一下專案中使用JPA的一些高階複雜場景的實踐指導,覆蓋了主要核心的JPA使用場景,可以讓你在需求開發的時候對JPA的使用更加的遊刃有餘。
Repository
上一篇文件中,我們知道業務程式碼中直接呼叫Repository
層中預設提供的方法或者是自己自定義的介面方法,便可以進行DB的相關操作。這裡我們再對repository的整體實現情況進一步探索下。
repository全貌梳理
先看下Repository相關的類圖:
整體類圖雖然咋看上去很龐雜,但其實主線脈絡還是比較清晰的。
- 先看下藍色的部分其實就是Repository的一整個介面定義鏈條,而橙色的則是我們自己自定義的一些Repository介面類,繼承父層介面的所有已有能力。
- 左側的類圖與介面,其實都是JPA提供的一些用於實現或者定製查詢操作的一些輔助實現類,後面章節中會看到他們的身影。
對主體repository層級提供的主要方法進行簡單的梳理,如下:
下面對各個repository介面進行簡單的獨立介紹。
JpaRepository與它的父類們
Repository
位於Spring Data Common
的lib裡面,是Spring Data 裡面做資料庫操作的最底層的抽象介面、最頂級的父類,原始碼裡面其實什麼方法都沒有,僅僅起到一個標識作用。CrudRepository
作為直接繼承Repository
的次頂層介面類,看名字也可以大致猜測出其主要作用就是封裝提供基礎CRUD操作。PagingAndSortingRepository
繼承自CrudRepository
,自然也就具備了CrudRepository
提供的全部介面能力。此外,從其自身新提供的介面來看,增加了排序和分頁查詢列表的能力,非常符合其類名的含義。
JpaRepository
與其前面的幾個父類相比是個特殊的存在,其中補充新增了一組JPA規範的介面方法。前面的幾個介面類都是Spring Data為了相容NoSQL而進行的一些抽象封裝(因為SpringData專案是一個龐大的家族,支援各種SQL與NoSQL的資料庫,SpringData JPA是SpringData家族中面向SQL資料庫的一個子分支專案),從JpaRepository
開始是對關係型資料庫進行抽象封裝。
從類圖可以看得出來它繼承了PagingAndSortingRepository
類,也就繼承了其所有方法,並且實現類也是SimpleJpaRepository
。從類圖上還可以看出JpaRepository
繼承和擁有了QueryByExampleExecutor
的相關方法。
通過原始碼和CrudRepository
相比較,它支援Query By Example,批量刪除,提高刪除效率,手動重新整理資料庫的更改方法,並將預設實現的查詢結果變成了List。
額外補充一句:
實際的專案編碼中,大部分的場景中,我們自定義Repository都是繼承
JpaRepository
來實現的。
自定義Repository
先看個自定義Repository的例子,如下:
看下對應類圖結構,自定義Repository繼承了JpaRepository,具備了其父系所有的操作介面,此外,額外擴充套件了業務層面自定義的一些介面方法:
自定義Repository
的時候,繼承JpaRepository需要傳入兩個泛型:
- 此Repository需要操作的具體Entity物件(Entity與具體DB中表對映,所以指定Entity也等同於指定了此Repository所對應的目標操作Table),
- 此Entity實體的主鍵資料型別(也就是第一個引數指定的Entity類中以@Id註解標識的欄位的型別)
分頁、排序,一招搞定
分頁,排序使用Pageable
物件進行傳遞,其中包含Page
和Sort
引數物件。
查詢的時候,直接傳遞Pageable
引數即可(注意下,如果是用原生SQL查詢的方式,此法行不通,後文有詳細說明)。
// 定義repository介面的時候,直接傳入Pageable引數即可
List<UserEntity> findAllByDepartment(DepartmentEntity department, Pageable pageable);
還有一種特殊的分頁場景。比如,DB表中有100w條記錄,然後現在需要將這些資料全量的載入到ES中。如果逐條查詢然後插入ES,顯然效率太慢;如果一次性全部查詢出來然後直接往ES寫,服務端記憶體可能會爆掉。
這種場景,其實可以基於Slice
結果物件進行實現。Slice的作用是,只知道是否有下一個Slice
可用,不會執行count,所以當查詢較大的結果集時,只知道資料是足夠的就可以了,而且相關的業務場景也不用關心一共有多少頁。
private <T extends EsDocument, F> void fullLoadToEs(IESLoadService<T, F> esLoadService) {
try {
final int batchHandleSize = 10000;
Pageable pageable = PageRequest.of(0, batchHandleSize);
do {
// 批量載入資料,返回Slice型別結果
Slice<F> entitySilce = esLoadService.slicePageQueryData(pageable);
// 具體業務處理邏輯
List<T> esDocumentData = esLoadService.buildEsDocumentData(entitySilce);
esUtil.batchSaveOrUpdateAsync(esDocumentData);
// 獲取本次實際上載入到的具體資料量
int pageLoadedCount = entitySilce.getNumberOfElements();
if (!entitySilce.hasNext()) {
break;
}
// 自動重置page分頁引數,繼續拉取下一批資料
pageable = entitySilce.nextPageable();
} while (true);
} catch (Exception e) {
log.error("error occurred when load data into es", e);
}
}
複雜搜尋,其實不復雜
按照條件進行搜尋查詢,是專案中遇到的非常典型且常用的場景。但是條件搜尋也分幾種場景,下面分開說下。
簡單固定場景
所謂簡單固定,即查詢條件就是固定的1個欄位或者若干個欄位,且查詢欄位數量不會變,比如根據部門查詢具體人員列表這種。
這種情況,我們可以簡單的直接在repository中,根據命名規範定義一個介面即可。
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 根據一個固定欄位查詢
List<UserEntity> findAllByDepartment(DepartmentEntity department);
// 根據多個固定欄位組合查詢
UserEntity findFirstByWorkIdAndUserNameAndDepartment(String workId, String userName, DepartmentEntity department);
}
簡單不固定場景
考慮一種場景,介面上需要做一個使用者搜尋的能力,要求支援根據使用者名稱、工號、部門、性別、年齡、職務等等若干個欄位中的1個或者多個的組合來查詢符合條件的使用者資訊。
顯然,上述通過直接在repository中按照命名規則定義介面的方式行不通了。這個時候,Example
物件便排上用場了。
其實在前面整體介紹Repository的UML圖中,就已經有了Example
的身影了,雖然這個名字起的很敷衍,但其功能確是挺實在的。
看下具體用法:
public Page<UserEntity> queryUsers(Request request, UserEntity queryParams) {
// 查詢條件構造出對應Entity物件,轉為Example查詢條件
Example<UserEntity> example = Example.of(queryParams);
// 構造分頁引數
Pageable pageable = PageHelper.buildPageable(request);
// 按照條件查詢,並分頁返回結果
return userRepository.findAll(example, pageable);
}
複雜場景
如果是一些自定義的複雜查詢場景,可以通過定製SQL語句的方式來實現。
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query(
value = "select t.*,(select group_concat(a.assigner_name) from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id) deal_person,"
+ " (select a.task_name from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id limit 1) cur_step "
+ " from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) order by t.create_time desc",
countQuery = "select count(1) from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) ",
nativeQuery = true)
Page<FlowResource> queryResource(List<String> type, String workId, Pageable pageable);
}
此外,還可以基於JpaSpecificationExecutor
提供的能力介面來實現。
自定義介面需要增加JpaSpecificationExecutor
的繼承,然後利用Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
介面來實現複雜查詢能力。
// 增加對JpaSpecificationExecutor的繼承
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long>, JpaSpecificationExecutor<UserEntity> {
}
public List<UserEntity> queryUsers(QueryParams queryParams) {
// 構造Specification查詢條件
Specification<UserEntity> specification =
(root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 範圍查詢條件構造
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), queryParams.getMinAge()));
predicates.add(cb.lessThanOrEqualTo(root.get("age"), queryParams.getMaxAge()));
// 精確匹配查詢條件構造
predicates.add(cb.equal(root.get("department"), queryParams.getDepartment()));
// 關鍵字模糊匹配條件構造
if (Objects.nonNull(queryParams.getNameKeyword())) {
predicates.add(cb.like(root.get("userName"), "%" + queryParams.getNameKeyword() + "%"));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
// 執行復雜查詢條件
return userRepository.findAll(specification);
}
自定義Listener,玩出花樣
實際專案中,經常會有一種場景,就是需要監聽某個資料的變更然後做一些額外的處理邏輯。一種邏輯,是寫操作的時候順便呼叫下相關業務的處理API,這樣會造成業務間耦合加深;優化點的策略是搞個MQ佇列,然後在這個寫DB操作的同時發個訊息到MQ裡面,然後一堆的consumer會監聽MQ並去做對應的處理邏輯,這樣引入個訊息佇列代價也有點高。
這個時候,我們可以藉助JPA的自定義EntityListener
功能來完美解決。通過監聽某個Entity表的變更情況,通知或者呼叫相關其他的業務程式碼處理,完美實現了與主體業務邏輯的解耦,也無需引入其他元件。
舉個例子:現有一個論壇發帖系統,發帖Post和評論Comment屬於兩個相對獨立又有點關係的資料,現在需要檢測當評論變化的時候,需要更新下Post對應記錄的評論數字段。下面演示下具體實現。
- 首先,定製一個Listener類,並指定Callbacks註解
public class CommentCountAuditListener {
/**
* 當Comment表有新增資料的操作時,觸發此方法的呼叫
*/
@PostPersist
public void postPersist(CommentEntity entity) {
// 執行Post表中評論數字段的更新
// do something here...
}
/**
* 當Comment表有刪除資料的操作時,觸發此方法的呼叫
*/
@PostRemove
public void postRemove(CommentEntity entity) {
// 執行Post表中評論數字段的更新
// do something here...
}
/**
* 當Comment表有更新資料的操作時,觸發此方法的呼叫
*/
@PostUpdate
public void postUpdate(CommentEntity entity) {
// 執行Post表中評論數字段的更新
// do something here...
}
}
- 其次,在評論實體CommentEntity上,加上自定義Listener資訊
@Entity
@Table("t_comment")
// 指定前面定製的Listener
@EntityListeners({CommentCountAuditListener.class})
public class CommentEntity extends AbstractAuditable {
// ...
}
這樣就搞定了。
自定義Listener還有個典型的使用場景,就是可以統一的記錄DB資料的操作日誌。
定製化SQL,隨心所欲
JPA提供@Query註解,可以實現自定義SQL語句的能力。比如:
@Query(value = "select * from user " +
"where work_id in (?1) " +
"and department_id = 0 " +
"order by CREATE_TIME desc ",
nativeQuery = true)
List<OssFileInfoEntity> queryUsersByWorkIdIn(List<String> workIds);
如果需要執行寫操作SQL的時候,需要額外增加@Modifying註解標識,如下:
@Modifying
@Query(value = "insert into user (work_id, user_name) values (?1, ?2)",
nativeQuery = true)
int createUser(String workId, String userName);
其中,nativeQuery = true
表示@Query
註解中提供的value值為原生SQL語句。如果nativeQuery
未設定或者設定為false,則表示將使用JPQL
語言來執行。所謂JPQL,即JAVA持久化查詢語句,是一種類似SQL的語法,不同點在於其使用類名來替代表名,使用類欄位來替代表欄位名。比如:
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name);
幾個關注點要特別闡述下:
- like查詢的時候,引數前後的
%
需要手動新增,系統是不會自動加上的
// like 需要手動新增百分號
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName like %?1")
public UserInfo getUserInfoByName(String name);
- 使用
nativeQuery=true
查詢的時候(原生SQL方式),不支援API介面裡面傳入Sort物件然後進行混合執行
// 錯誤示範: 自定義sql與API中Sort引數不可同時混用
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1", nativeQuery=true)
public UserInfo getUserInfoByName(String name, Sort sort);
// 正確示範: 自定義SQL完成對應sort操作
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn);
- 未指定
nativeQuery=true
查詢的時候(JPQL方式),支援API介面裡面傳入Sort
、PageRequest
等物件然後進行混合執行,來完成排序、分頁等操作
// 正確:自定義jpql與API中Sort引數不可同時混用
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name, Sort sort);
- 支援使用引數名作為
@Query
查詢中的SQL或者JPQL語句的入參,取代引數順序佔位符
預設情況下,引數是通過順序繫結在自定義執行語句上的,這樣如果API介面傳參順序或者位置改變,極易引起自定義查詢傳參出問題,為了解決此問題,我們可以使用@Param
註解來繫結一個具體的引數名稱,然後以引數名稱的形式替代位置順序佔位符,這也是比較推薦的一種做法。
// 預設的順序位置傳參
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn);
// 使用引數名稱傳參
@Query("SELECT * FROM t_user u WHERE u.user_name = :name order by :sortColumn", nativeQuery=true)
public UserInfo getUserInfoByName(@Param("name") String name, @Param("sortColumn") String sortColumn);
欄位命名對映策略
一般而言,JAVA的編碼規範都要求filed欄位命名需要遵循小駝峰命名的規範,比如userName,而DB中column命名的時候,很多人習慣於使用下劃線分隔的方式命名,比如user_name
這種。這樣就涉及到一個對映的策略問題,需要讓JPA知道程式碼裡面的userName就對應著DB中的user_name
。
這裡就會涉及到對命名對映策略的對映。主要有兩種對映配置,下面分別闡述下。
- implicit-strategy
配置項key值:
spring.jpa.hibernate.naming.implicit-strategy=xxxxx
取值說明:
值 | 對映規則說明 |
---|---|
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImp | 預設的命名策略,相容JPA2.0規範 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl | 相容老版本Hibernate的命名規範 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl | 與ImplicitNamingStrategyJpaCompliantImp基本相同 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl | 相容JPA 1.0規範中的命名規範。 |
org.hibernate.boot.model.naming.SpringImplicitNamingStrategy | 繼承ImplicitNamingStrategyJpaCompliantImpl,對外來鍵、連結串列查詢、索引如果未定義,都有下劃線的處理策略,而table和column名字都預設與欄位一樣 |
- physical-strategy
配置項key值:
spring.jpa.hibernate.naming.physical-strategy=xxxxx
取值說明:
值 | 對映規則說明 |
---|---|
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl | 預設字串一致對映,不做任何轉換處理,比如java類中userName,對映到table中列名也叫userName |
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy | java類中filed名稱小寫字母進行對映到DB表column名稱,遇大寫字母時轉為分隔符"_"命名格式,比如java類中userName欄位,對映到DB表column名稱叫user_name |
- physical-strategy與implicit-strategy
SpringData JPA只是對JPA規範的二次封裝,其底層使用的是Hibernate
,所以此處涉及到Hibernate提供的一些處理策略。Hibernate將物件模型對映到關聯式資料庫分為兩個步驟:
- 從物件模型中確定邏輯名稱。邏輯名可以由使用者顯式指定(使用
@Column
或@Table
),也可以隱式指定。 - 將邏輯名稱對映到物理名稱,也就是資料庫中使用的名稱。
這裡,implicit-strategy
用於第一步隱式指定邏輯名稱,而physical-strategy
則用於第二步中邏輯名稱到物理名稱的對映。
注意:
當沒有使用@Table
和@Column
註解時,implicit-strategy
配置項才會被使用,即implicit-strategy
定義的是一種預設場景的處理策略;而physical-strategy
屬於一種高優先順序的策略,只要設定就會被執行,而不管是否有@Table
和@Column
註解。
小結,承上啟下
好啦,本篇內容就介紹到這裡。
通過本篇的內容,我們對於如何在專案中使用Spring Data JPA
來進行一些較為複雜場景的處理方案與策略有了進一步的瞭解,再結合本系列此前的內容,到此掌握的JPA的相關技能已經足以應付大部分專案開發場景。
在實際專案中,為了保障資料操作的可靠、避免髒資料的產生,需要在程式碼中加入對資料庫操作的事務控制。在下一篇文件中,我們將一起聊一聊Spring Data JPA業務程式碼開發中關於資料庫事務的控制,以及編碼中存在哪些可能會導致事務失效的場景等等。
如果對本文有自己的見解,或者有任何的疑問或建議,都可以留言,我們一起探討、共同進步。
補充
Spring Data JPA
作為Spring Data
中對於關係型資料庫支援的一種框架技術,屬於ORM
的一種,通過得當的使用,可以大大簡化開發過程中對於資料操作的複雜度。本文件隸屬於《
Spring Data JPA
用法與技能探究》系列的第3篇。本系列文件規劃對Spring Data JPA
進行全方位的使用介紹,一共分為5篇文件,如果感興趣,歡迎關注交流。《Spring Data JPA用法與技能探究》系列涵蓋內容:
- 開篇介紹 —— 《Spring Data JPA系列1:JDBC、ORM、JPA、Spring Data JPA,傻傻分不清楚?給你個選擇SpringDataJPA的理由!》
- 快速上手 —— 《Spring Data JPA系列2:SpringBoot整合JPA詳細教程,快速在專案中熟練使用JPA》
- 深度進階 —— 《Spring Data JPA系列3:JPA專案中核心場景與進階用法介紹》
- 可靠保障 —— 《聊一聊資料庫的事務,以及Spring體系下對事務的使用》
- 周邊擴充套件 —— 《JPA開發輔助效率提升方案介紹》
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點個關注,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。