利用Spring-Data-Jpa中的QueryByExample和SpecificationExecutor兩個接口實現複雜業務場景的數據庫查詢

在之前有關Spring-Data-Jpa的文章中,筆者實現了基本的CRUD操作和分頁及排序查找功能,還不足以應對工作中出現的複雜業務場景。那麼本文就來帶領大家利用Spring-Data-Jpa中的QueryByExampleExecutorJpaSpecificationExecutor兩個接口實現相對複雜的業務場景,相信看完本文,讀者對於使用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的使用示例

  1. 將bootDemo項目下的UserRepository接口改爲繼承自JpaRepository
public interface UserRepository extends JpaRepository<UserInfo,Long{
    
}
  1. UserService接口下新增兩個抽象方法,一個查詢單個對象,另一個查詢符合條件的集合
public interface UserService >{ 
   UserInfo findOneByExample(UserInfo userInfo);
   
   List<UserInfo> findAllByExample(UserInfo userInfo);  
   //其他抽象方法此處省略......
}
  1. 完成實現方法
@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;
    }
    //其他路由方法此處省略......
}

  1. 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對象,由 userInfomatcher共同創建,爲講解方便,我們先來明確一些定義:

(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-jpaQueryByExampleExecutorJpaSpecificationExecutor兩個接口中的方法完成複雜的數據庫業務查詢;
  • 同時擴展了JpaSpecificationExecutor創建了一個更加方便使用的工廠類SpecificationFactory
  • JpaSpecificationExecutor接口的實現類SimpleJpaRepository類的關鍵源碼進行了簡易分析;
  • 利用好JpaSpecificationExecutor接口中的API幾乎可以高效實現任意複雜場景需求的數據庫查詢

4 參考書籍

張振華著《Spring Data Jpa從入門到精通》之第6章:JpaRepository擴展詳解

歡迎掃描下方二維碼關注本人的微信公衆號,定期更新技術乾貨
微信公衆號名
注公衆號後發送消息【bootDemo項目源碼】可獲得本項目源碼地址

          -END-
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章