在之前有關Spring-Data-Jpa的文章中,筆者實現了基本的CRUD操作和分頁及排序查找功能,還不足以應對工作中出現的複雜業務場景。那麼本文就來帶領大家利用Spring-Data-Jpa中的
QueryByExampleExecutor
和JpaSpecificationExecutor
兩個接口實現相對複雜的業務場景,相信看完本文,讀者對於使用Spring-Data-Jpa實現複雜的數據庫查詢業務會有不少收穫。
本文的demo代碼構建在筆者的上一篇有關spring-data-jpa的文章
Spring-Data-Jpa中的常用註解詳解及其用法
1 QueryByExampleExecutor的使用
按示例查詢(QBE)是一種用戶友好的查詢技術,具有簡單的接 口。它允許動態查詢創建,並且不需要編寫包含字段名稱的查詢。從 UML圖中,可以看出繼承JpaRepository
接口後,自動擁有了按“實例”進行查詢的諸多方法。可見Spring Data的團隊已經認爲了QBE是 Spring JPA的基本功能了,繼承QueryByExampleExecutor
和繼承 JpaRepository
都會有這些基本方法。
1.1 QueryByExampleExecutor的詳細配置
public interface QueryByExampleExecutor<T> {
/**
* 根據樣例查找一個符合條件的對象,如果沒找到將返回null;如果返回多個對象時將拋出
*org.springframework.dao.IncorrectResultSizeDataAccessException異常
*/
<S extends T> Optional<S> findOne(Example<S> example);
/**
*根據樣例查找符合條件的所有對象集合
*/
<S extends T> Iterable<S> findAll(Example<S> example);
/**
*根據樣例查找符合條件的所有對象集合,並根據排序條件排好序
*/
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
/**
*根據樣例查找符合條件的所有對象集合,並根據分頁條件分頁
*/
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
/**
*查詢符合樣例條件的記錄數樣
*/
<S extends T> long count(Example<S> example);
/**
*檢查數據庫表中是否包含符合樣例條件的記錄,存在返回true,否則返回false
*/
<S extends T> boolean exists(Example<S> example);
}
所以我們看Example基本上就可以掌握的它的用法和API了。
注意: Example接口在org.springframework.data.domain
包下
public interface Example<T> {
/**
*創建一個泛型對象的樣例,泛型對象必須是與數據庫表中一條記錄對應的實體類
*/
static <T> Example<T> of(T probe) {
return new TypedExample<>(probe, ExampleMatcher.matching());
}
/**
* 根據實體類和匹配規則創建一個樣例
* @return
*/
static <T> Example<T> of(T probe, ExampleMatcher matcher) {
return new TypedExample<>(probe, matcher);
}
/**
*獲取樣例中的實體類對象
*/
T getProbe();
/**
*獲取樣例中的匹配器
*/
ExampleMatcher getMatcher();
/**
*獲取樣例中的實體類類型
*/
@SuppressWarnings("unchecked")
default Class<T> getProbeType() {
return (Class<T>) ProxyUtils.getUserClass(getProbe().getClass());
}
}
從源碼中可以看出Example主要包含三部分內容:
- Probe: 這是具有填充字段的域對象的實際實體類,即查詢條 的封裝類。必填。
- ExampleMatcher:ExampleMatcher有關於如何匹配特定字段的 匹配規則,它可以重複使用在 多個示例。必填。如果不填,用 默認的。
- Example:Example由探針和ExampleMatcher組成,它用於創建查詢。
1.2 QueryByExampleExecutor的使用示例
- 將bootDemo項目下的
UserRepository
接口改爲繼承自JpaRepository
public interface UserRepository extends JpaRepository<UserInfo,Long{
}
- UserService接口下新增兩個抽象方法,一個查詢單個對象,另一個查詢符合條件的集合
public interface UserService >{
UserInfo findOneByExample(UserInfo userInfo);
List<UserInfo> findAllByExample(UserInfo userInfo);
//其他抽象方法此處省略......
}
- 完成實現方法
@Service
@Slf4j
public class UserServiceImpl implements UserService{
//其他實現方法此處省略......
@Override
public UserInfo findOneByExample(UserInfo userInfo) {
//構建ExampleMatcher對象,matchingAll表示要匹配所有
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll();
exampleMatcher.withMatcher("userName", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.EXACT,true));
//利用Example類的靜態構造函數構造Example實例對象
Example<UserInfo> example = Example.of(userInfo,exampleMatcher);
return userRepository.findOne(example).get();
}
@Override
public List<UserInfo> findAllByExample(UserInfo userInfo) {
//匹配任意一個符合條件的字段
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll();
exampleMatcher.withMatcher("userRole",ExampleMatcher.GenericPropertyMatchers.startsWith());
exampleMatcher.withMatcher("userName",ExampleMatcher.GenericPropertyMatchers.startsWith());
//不區分大小寫
exampleMatcher.isIgnoreCaseEnabled();
Example<UserInfo> example = Example.of(userInfo,exampleMatcher);
return userRepository.findAll(example);
}
}
4) UserInfoController
類中新增兩個路由方法
@RestController
@RequestMapping("/user")
@Slf4j
public class UserInfoController {
@GetMapping("/example/{userName}")
public ServiceResponse<UserInfo> findOneByExampleUserName(@PathVariable("userName") String userName){
ServiceResponse<UserInfo> response = new ServiceResponse<>();
log.info("userName={}",userName);
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
UserInfo data = userInfoService.findOneByExample(userInfo);
response.setData(data);
return response;
}
@GetMapping("/example/list")
public ServiceResponse<List<UserInfo>> findListByExample(@RequestParam("userName") String userName,@RequestParam("userRole") String userRole){
ServiceResponse<List<UserInfo>> response = new ServiceResponse<>();
log.info("userName={},userRole={}",userName,userRole);
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setUserRole(userRole);
List<UserInfo> data = userInfoService.findAllByExample(userInfo);
response.setData(data);
return response;
}
//其他路由方法此處省略......
}
- postman測試
啓動項目後可利用postman對開發的http接口進行測試
GET http://localhost:8088/apiBoot/user/example/ZhuGeLiang
//接口響應信息
{
"status": 200,
"message": "ok",
"data": {
"userId": 22,
"userName": "ZhuGeLiang",
"password": "cea79d52d2117875eb9d377bfe68f65e",
"userNameCn": "諸葛亮",
"userSex": "M",
"userRole": "Admin",
"telNum": 15200001309,
"email": "[email protected]",
"regDate": "2019-03-06",
"birthDay": "1972-10-08",
"createdBy": "system",
"createdTime": "2020-04-30 10:00:00",
"lastUpdatedBy": "x_heshengfu",
"lastUpdatedTime": "2020-06-10 10:00:00"
}
}
//根據樣例查詢符合對象的集合
GET http://localhost:8088/apiBoot/user/example/list?userName=Zhangfei&userRole=Admin
//接口相應信息
{
"status": 200,
"message": "ok",
"data": [
{
"userId": 21,
"userName": "ZhangFei",
"password": "956c5c8200854fb09c24ec10144747d0",
"userNameCn": "張飛",
"userSex": "M",
"userRole": "Admin",
"telNum": 15200001308,
"email": "[email protected]",
"regDate": "2018-03-05",
"birthDay": "1969-08-01",
"createdBy": "system",
"createdTime": "2020-04-30 10:00:00",
"lastUpdatedBy": "x_heshengfu",
"lastUpdatedTime": "2020-06-10 10:00:00"
}
]
}
通過測試和日誌信息筆者發現通過樣例的ExampleMatcher.GenericPropertyMatchers.startsWith()
走的其實還是完全匹配,並不是起始匹配,後臺日誌中的sql參數化查詢信息如下:
select
userinfo0_.user_id as user_id1_1_,
userinfo0_.birth_day as birth_da2_1_,
userinfo0_.created_by as created_3_1_,
userinfo0_.created_time as created_4_1_,
userinfo0_.email as email5_1_,
userinfo0_.last_updated_by as last_upd6_1_,
userinfo0_.last_updated_time as last_upd7_1_,
userinfo0_.password as password8_1_,
userinfo0_.reg_date as reg_date9_1_,
userinfo0_.tel_num as tel_num10_1_,
userinfo0_.user_name as user_na11_1_,
userinfo0_.user_name_cn as user_na12_1_,
userinfo0_.user_role as user_ro13_1_,
userinfo0_.user_sex as user_se14_1_
from
user_info userinfo0_
where
userinfo0_.user_name=?
and userinfo0_.user_role=?
上面的代碼示例中是這樣創建實例的:Example.of(userInfo,exampleMatcher)
;我們看到,Example對象,由 userInfo
和matcher
共同創建,爲講解方便,我們先來明確一些定義:
(1)Probe:實體對象,在持久化框架中與Table對應的域對 象,一個對象代表數據庫表中的一條記錄,如上例中UserInfo對象。 在構建查詢條件時,一個實體對象代表的是查詢條件中的字段值部 分。如:要查詢姓名爲 “ZhangFei”的客戶,實體對象只能存儲條件值爲可忽略大小寫的“Zhangfei”。
(2)ExampleMatcher:匹配器,它是匹配“實體對象”的,表 示瞭如何使用“實體對象”中的“值”進行查詢,它代表的是“查詢 方式”,解釋瞭如何去查的問題。
(3)Example:實例對象,代表的是完整的查詢條件,由實體對象(查詢條件值)和匹配器(查詢方式)共同創建。
1.3 QueryByExampleExecutor的特點及約束
(1)支持動態查詢。即支持查詢條件個數不固定的情況,如:用戶列表中有多個過濾條件,用戶使用時在“用戶名”查詢框中輸入了值,就需要按用戶名進行過濾,如果沒有輸入值,就忽略這個過濾條件。對應的實現是,在構建查詢條件UserInfo對象時,將email屬性值設置爲具體的條件值或設置爲null。
(2)不支持過濾條件分組。即不支持過濾條件用or(或)來連 接,所有的過濾查件,都是簡單一層的用and(並且)連接。如 firstname = ?0 or (firstname = ?1 and lastname = ?2)。
(3)正是由於這個限 制,有些查詢是沒辦法支持的,例如要查詢某個時間段內添加的客 戶,對應的屬性是addTime,需要傳入“開始時間”和“結束時 間”兩個條件值,而這種查詢方式沒有存兩個值的位置,所以就沒辦法完成這樣的查詢。
1.4 ExampleMatcher詳解
1.4.1 源碼解讀
public interface ExampleMatcher {
/*
* 使用字符串匹配器
*/
ExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher);
/**
*忽略大小寫的匹配器
*/
default ExampleMatcher withIgnoreCase() {
return withIgnoreCase(true);
}
/**
*傳參決定是否忽略大小寫
*/
ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase);
/**
*根據與表字段對應的屬性名propertyPath和匹配配置器匹配
*/
default ExampleMatcher withMatcher(String propertyPath, MatcherConfigurer<GenericPropertyMatcher> matcherConfigurer) {
Assert.hasText(propertyPath, "PropertyPath must not be empty!");
Assert.notNull(matcherConfigurer, "MatcherConfigurer must not be empty!");
GenericPropertyMatcher genericPropertyMatcher = new GenericPropertyMatcher();
matcherConfigurer.configureMatcher(genericPropertyMatcher);
return withMatcher(propertyPath, genericPropertyMatcher);
}
/**
*同上,第二個參數爲GenericPropertyMatcher類型
*/
ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);
//其他源碼省略
static ExampleMatcher matchingAll() {
return new TypedExampleMatcher().withMode(MatchMode.ALL);
}
}
ExampleMatcher接口的實現類爲TypedExampleMatcher
類
1.4.2 關鍵屬性分析
(1)nullHandler:Null值處理方式,枚舉類型,有2個可選值:
- INCLUDE(包括)
- IGNORE(忽略)
標識作爲條件的實體對象中,一個屬性值(條件值)爲Null時, 表示是否參與過濾。當該選項值是INCLUDE時,表示仍參與過濾,會匹配數據庫表中該字段值是Null的記錄;若爲IGNORE值,表示不參與 過濾。
(2)defaultStringMatcher:默認字符串匹配方式,枚舉類 型,有6個可選值:
- DEFAULT(默認,效果同EXACT)
- EXACT(相等)
- STARTING(開始匹配)
- ENDING(結束匹配)
- CONTAINING(包含,模糊匹配)
- REGEX(正則表達式)
本人親測試過程中發現除了EXACT精確匹配,其他都不生效,所以就不深入研究了
2 JpaSpecificationExecutor的詳細使用
JpaSpecificationExecutor
是JPA 2.0提供的Criteria API,可 以用於動態生成query
。Spring Data JPA支持Criteria查詢,可以很 方便地使用,足以應付工作中的所有複雜查詢的情況了,可以對JPA實現最大限度的擴展。
2.1 JpaSpecificationExecutor的使用方法
public interface JpaSpecificationExecutor<T> {
//根據Specificatio條件查詢單個結果
Optional<T> findOne(@Nullable Specification<T> spec);
//根據Specificatio條件查詢List結果集
List<T> findAll(@Nullable Specification<T> spec);
//根據Specificatio條件分頁查詢
Page<T> findAll(@Nullable Specification<T> spec, Pageable page);
//根據Specificatio條件查詢並排序
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
//根據Specificatio條件查詢符合條件的數量
long count(@Nullable Specification<T> spec);
}
這個接口基本是圍繞着Specification接口來定義的, Specification接口中只定義瞭如下一個方法:
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
所以可看出,JpaSpecificationExecutor
是針對Criteria API
進 行了predicate
標準封裝,幫我們封裝了通過EntityManager
的查詢和 使用細節,操作Criteria
更加便利了一些
2.1 Criteria概念的簡單介紹
(1)Root<T>root
:代表了可以查詢和操作的實體對象的根。如 果將實體對象比喻成表名,那root裏面就是這張表裏面的字段。這不 過是JPQL的實體字段而已。通過裏面的Path<Y>get(String attributeName)
來獲得我們操作的字段。
(2)CriteriaQuery<?>query
:代表一個specific的頂層查詢對 象,它包含着查詢的各個部分,比如:select、from、where、group by、order by
等。CriteriaQuery
對象只對實體類型或嵌入式類型的 Criteria
查詢起作用,簡單理解,它提供了查詢ROOT的方法。常用的方法有:
//單個查詢
CriteriaQuery<T> select(Selection<? extends T> selection);
//多個查詢,等同於聯合查詢
CriteriaQuery<T> multiselect(Selection... selections);
//where 條件過濾
CriteriaQuery<T> where(Predicate... restrictions);
//分組查詢
CriteriaQuery<T> groupBy(Expression... expressions);
//having過濾
CriteriaQuery<T> having(Predicate... restrictions);
(3)CriteriaBuilder cb:用來構建CritiaQuery的構建器對 象,其實就相當於條件或者是條件組合,以謂語即Predicate的形式 返回。構建簡單的Predicate示例:
Predicate p1 = cb.like(root.get("name").as(String.class),"%"+param.getName()+"%");
Predicate p2 = cb.equal(root.get("uuid").as(Integer.class),param.getUuid());
Predicate p3 = cb.gt(root.get("age").as(Integer.class),param.getAge());
//構建組合的Predicate示例:
Predicate p = cb.and(p3,cb.or(p1,p2));
(4) 實際經驗:到此我們發現其實JpaSpecificationExecutor 幫我們提供了一個高級的入口和結構,通過這個入口,可以使用底層 JPA的Criteria的所有方法,其實就可以滿足了所有業務場景。但實 際工作中,需要注意的是,如果一旦我們寫的實現邏輯太複雜,一般的程序員看不懂的時候,那一定是有問題的,我們要尋找更簡單的, 更易懂的,更優雅的方式。比如:
- 分頁和排序我們就沒有自己再去實現一遍邏輯,直接用其開放的Pageable和Sort即可。
- 當過多地使用group或者having、sum、count等內置的SQL函數 的時候,我們想想就是我們通過Specification實現了邏輯, 這種效率真的高嗎?是不是數據的其他算法更好?
- 當我們過多地操作left join和inner Join鏈表查詢的時候, 我們想想,是不是通過數據庫的視圖(view)更優雅一點?
2.2 JpaSpecificationExecutor使用示例
繼續以user_info表爲被查詢的表作演示
(1) 新建一個用於動態查詢的參數類UserParam
@NoArgsConstructor
public class UserParam implements Serializable {
@Setter
@Getter
private String userName;
@Setter
@Getter
private String userNameCn;
@Setter
@Getter
private String userSex;
@Setter
@Getter
private String email;
@Setter
@Getter
private Long telNum;
@Setter
@Getter
private String beginCreateTime;
@Setter
@Getter
private String endCreateTime;
}
(2)在配置類中配置一個JpaSpecificationExecutor
接口實現類SimpleJpaRepository
的bean
@Configuration
public class BeansConfiguration {
@Autowired
private EntityManager entityManager;
@Bean("userSpecificationRepository")
public SimpleJpaRepository userSpecificationRepository(){
//構造SimpleJpaRepository實例時需要注入EntityManager實例
return new SimpleJpaRepository<UserInfo,Long>(UserInfo.class,entityManager);
}
}
注意:在2.1版本以上的spring-boot-data-jpa
中僅通過定義一個繼承自JpaSpecificationExecutor
的接口是行不通的,那樣會導致項目啓動時報錯,無法創建JpaSpecificationExecutor
接口對應的實現類bean
(3)UserService
接口中新建一個動態查詢的抽象方法
List<UserInfo> findAllByDynamicConditions(UserParam userParam);
(4) UserServiceImpl
類中注入simpleJpaRepository
,調用findAll(Specification<T> spec)
方法完成動態查詢邏輯
@Autowired
private SimpleJpaRepository<UserInfo,Long> simpleJpaRepository;
@Override
public List<UserInfo> findAllByDynamicConditions(UserParam userParam) {
return simpleJpaRepository.findAll((root,query,cb)->{
List<Predicate> predicates = new ArrayList<>();
if(!StringUtils.isEmpty(userParam.getUserName())){
predicates.add(cb.like(root.get("userName"),userParam.getUserName()+"%"));
}
if(!StringUtils.isEmpty(userParam.getUserNameCn())){
predicates.add(cb.like(root.get("userNameCn"),userParam.getUserNameCn()+"%"));
}
if(!StringUtils.isEmpty(userParam.getUserSex())){
predicates.add(cb.equal(root.get("userSex"),userParam.getUserSex()));
}
if(userParam.getTelNum()!=null){
predicates.add(cb.equal(root.get("telNum"),userParam.getTelNum()));
}
if(!StringUtils.isEmpty(userParam.getEmail())){
predicates.add(cb.like(root.get("email"),userParam.getEmail()+"%"));
}
//根據時間區間查詢
if(userParam.getBeginCreateTime()!=null && userParam.getEndCreateTime()!=null){
predicates.add(cb.between(root.get("createdTime"),userParam.getBeginCreateTime(),userParam.getEndCreateTime()));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
});
}
(5) UserInfoController
類中完成動態查詢路由方法
@PostMapping("/list/conditions")
public ServiceResponse<List<UserInfo>> findUsersByConditions(@RequestBody UserParam userParam){
log.info("userParam={}",JSON.toJSON(userParam));
ServiceResponse<List<UserInfo>> response = new ServiceResponse<>();
List<UserInfo> data = userInfoService.findAllByDynamicConditions(userParam);
response.setData(data);
return response;
}
(6)postman測試接口效果
重啓服務器後可利用postman對新開發的http接口進行測試
/**postman軟件
*請求類型選擇POST
*URL欄填寫:http://localhost:8088/apiBoot/user/list/conditions
*入參body選擇raw類型,json格式 如上圖所示
*/
//接口入參
{
"userName":"Zh",
"beginCreateTime":"2020-03-10 00:00:00",
"endCreateTime": "2020-04-30 10:00:00"
}
//接口相應信息
{
"status": 200,
"message": "ok",
"data": [
{
"userId": 21,
"userName": "ZhangFei",
"password": "956c5c8200854fb09c24ec10144747d0",
"userNameCn": "張飛",
"userSex": "M",
"userRole": "Admin",
"telNum": 15200001308,
"email": "[email protected]",
"regDate": "2018-03-05",
"birthDay": "1969-08-01",
"createdBy": "system",
"createdTime": "2020-04-30 10:00:00",
"lastUpdatedBy": "x_heshengfu",
"lastUpdatedTime": "2020-06-10 10:00:00"
},
{
"userId": 1,
"userName": "ZhangSan",
"password": "2060a7a94bbf5d5fbec8ca4b1f7337d6",
"userNameCn": "張三",
"userSex": "M",
"userRole": "Developer",
"telNum": 13100001001,
"email": "[email protected]",
"regDate": "2018-10-10",
"birthDay": "1990-05-18",
"createdBy": "system",
"createdTime": "2020-03-13 23:45:35",
"lastUpdatedBy": "admin",
"lastUpdatedTime": "2020-04-26 11:28:29"
},
{
"userId": 25,
"userName": "ZhouYu",
"password": "8b9e0e71284ee5110b98ea9f3ecef61d",
"userNameCn": "周瑜",
"userSex": "M",
"userRole": "Developer",
"telNum": 15200001312,
"email": "[email protected]",
"regDate": "2018-04-05",
"birthDay": "1972-08-10",
"createdBy": "system",
"createdTime": "2020-04-30 10:00:00",
"lastUpdatedBy": "x_heshengfu",
"lastUpdatedTime": "2020-06-10 10:00:00"
},
{
"userId": 22,
"userName": "ZhuGeLiang",
"password": "cea79d52d2117875eb9d377bfe68f65e",
"userNameCn": "諸葛亮",
"userSex": "M",
"userRole": "Admin",
"telNum": 15200001309,
"email": "[email protected]",
"regDate": "2019-03-06",
"birthDay": "1972-10-08",
"createdBy": "system",
"createdTime": "2020-04-30 10:00:00",
"lastUpdatedBy": "x_heshengfu",
"lastUpdatedTime": "2020-06-10 10:00:00"
}
]
}
實際工作中應該大部分都是這種寫法, 就算擴展也是百變不離其宗。
接口響應信息說明spring-data-jpa實現的動態查詢時可行而簡便的,測試過程中後臺系統打印出瞭如下sql 預編譯查詢日誌信息:
select
userinfo0_.user_id as user_id1_1_,
userinfo0_.birth_day as birth_da2_1_,
userinfo0_.created_by as created_3_1_,
userinfo0_.created_time as created_4_1_,
userinfo0_.email as email5_1_,
userinfo0_.last_updated_by as last_upd6_1_,
userinfo0_.last_updated_time as last_upd7_1_,
userinfo0_.password as password8_1_,
userinfo0_.reg_date as reg_date9_1_,
userinfo0_.tel_num as tel_num10_1_,
userinfo0_.user_name as user_na11_1_,
userinfo0_.user_name_cn as user_na12_1_,
userinfo0_.user_role as user_ro13_1_,
userinfo0_.user_sex as user_se14_1_
from
user_info userinfo0_
where
(
userinfo0_.user_name like ?
)
and (
userinfo0_.created_time between ? and ?
)
2.3 Specification工作中的一些擴展
我們在實際工作中會發現,如果按上面的邏輯,簡單重複,總感 覺是不是可以抽出一些公用方法呢,此時我們引入一種工廠模式,幫 我們做一些事情。基於JpaSpecificationExecutor
的思路,我們創建一個SpecificationFactory.Java
,內容如下:
public final class SpecificationFactory {
/**
* 模糊查詢,匹配對應字段
* @param attribute
* @param value
* @return
*/
public static Specification containsLike(String attribute,String value){
return (root, query, cb) -> cb.like(root.get(attribute),"%"+value+"%");
}
/**
* 獲取某字段等於value的查詢條件
* @param attribute
* @param value
* @return
*/
public static Specification equal(String attribute,Object value){
return (root,query,cb)->cb.equal(root.get(attribute),value);
}
/**
* 插敘某字段在一個區間的範圍
* @param attribute
* @param min
* @param max
* @return
*/
public static Specification isBetween(String attribute,int min,int max){
return (root,query,cb)->cb.between(root.get(attribute),min,max);
}
public static Specification isBetween(String attribute,double min,double max){
return (root,query,cb)->cb.between(root.get(attribute),min,max);
}
public static Specification isBetween(String attribute, Date min, Date max){
return (root,query,cb)->cb.between(root.get(attribute),min,max);
}
/**
* 通過屬性名和集合實現In查詢
* @param attribute
* @param c
* @return
*/
public static Specification in(String attribute, Collection c){
return (root,query,cb)->root.get(attribute).in(c);
}
public static Specification greaterThan(String attribute, BigDecimal value){
return (root,query,cb)->cb.greaterThan(root.get(attribute),value);
}
public static Specification greaterThan(String attribute, Long value){
return (root,query,cb)->cb.greaterThan(root.get(attribute),value);
}
}
可以根據實際工作需要和場景進行不斷擴充
調用示例1:
@Override
public List<UserInfo> findAllByContainsLike(String attribute, String value) {
return simpleJpaRepository.findAll(SpecificationFactory.containsLike(attribute,value));
}
配合Specification使用,調用示例2:
@Override
public List<UserInfo> findAllByContainsLikeAndBetween(String attribute, String value, Date min, Date max) {
return simpleJpaRepository.findAll(SpecificationFactory
.containsLike(attribute,value)
.and(SpecificationFactory.isBetween("createdTime",min,max)));
}
Specification是Spring Data JPA對Specification的聚合操作工具類,裏面有以下4個方法:
static <T> Specification<T> not(Specification<T> spec) {
return Specifications.negated(spec);
}
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, CompositionType.AND);
}
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, CompositionType.OR);
}
2.4 JpaSpecificationExecutor實現原理
(1)在IDEA中打開SimpleJpaRepository
類右鍵選擇Diagram彈出如下圖所示的類繼承和實現接口關係圖:
由上圖可以看出SimpleJpaRepository
類實現了JPA中大部分的Repository接口
(2)SimpleJpaRepository實現類中的關鍵源碼:
/**
*以findOne爲例
*/
public Optional<T> findOne(@Nullable Specification<T> spec) {
try {
return Optional.of(this.getQuery(spec, Sort.unsorted()).getSingleResult());
} catch (NoResultException var3) {
return Optional.empty();
}
}
/**
*解析Specification,利用EntityManager直接實現調用邏輯
*/
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
if (sort.isSorted()) {
query.orderBy(QueryUtils.toOrders(sort, root, builder));
}
return this.applyRepositoryMethodMetadata(this.em.createQuery(query));
}
3 小結
- 本文主要講解了使用
spring-data-jpa
中QueryByExampleExecutor
和JpaSpecificationExecutor
兩個接口中的方法完成複雜的數據庫業務查詢; - 同時擴展了
JpaSpecificationExecutor
創建了一個更加方便使用的工廠類SpecificationFactory
; - 對
JpaSpecificationExecutor
接口的實現類SimpleJpaRepository
類的關鍵源碼進行了簡易分析; - 利用好
JpaSpecificationExecutor
接口中的API幾乎可以高效實現任意複雜場景需求的數據庫查詢
4 參考書籍
張振華著《Spring Data Jpa從入門到精通》之第6章:JpaRepository
擴展詳解
歡迎掃描下方二維碼關注本人的微信公衆號,定期更新技術乾貨
注公衆號後發送消息【bootDemo項目源碼】可獲得本項目源碼地址
-END-