Spring Data JPA進階——Specifications和Querydsl


Spring Data JPA進階——Specifications和Querydsl

本篇介紹一下spring Data JPA中能為數據訪問程序的開發帶來更多便利的特性,我們知道,Spring Data repository的配置很簡單,一個典型的repository像下面這樣:

public interface CustomerRepository extends JpaRepository<Customer, Long> { Customer findByEmailAddress(String emailAddress); List<Customer> findByLastname(String lastname, Sort sort); Page<Customer> findByFirstname(String firstname, Pageable pageable); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

第一個方法表示根據email查詢一個Customer,第二個方法表示根據lastName和排序條件查詢一個Customer的集合,第三個方法表示根據fristName和分頁的信息查詢一頁Customer

這樣的方式非常簡單,甚至不用編寫方法的實現就可以實現查詢的功能,但是這仍然有個弊端,如果查詢條件增長,方法會越來越多,如果能動態的組裝查詢條件就好了

那么,可以嗎?答案當然是yes

我們都知道JPA提供了Criteria API,下面我們就用一個例子,展示一下Criteria的使用,想象這樣一個場景,我們想針對長期客戶,在生日那天給他發一段祝福,我們怎么做呢?

使用Criteria API

我們有兩個條件,生日和長期客戶,我們假設兩年前注冊的就是長期客戶吧,怎么用JPA 2.0的Criteria API實現呢:

LocalDate today = new LocalDate(); CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<Customer> query = builder.createQuery(Customer.class); Root<Customer> root = query.from(Customer.class); Predicate hasBirthday = builder.equal(root.get(Customer_.birthday), today); Predicate isLongTermCustomer = builder.lessThan(root.get(Customer_.createdAt), today.minusYears(2); query.where(builder.and(hasBirthday, isLongTermCustomer)); em.createQuery(query.select(root)).getResultList();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我們先創建了一個LocalDate對象,然后是三行樣板代碼啊,后面兩行是建立查詢條件,然后通過where子句連在一起,然后執行查詢

上面查詢有兩個問題

  • 第一,由於每次要先建立CriteriaBuilder,CriteriaQuery,Root,所以導致查詢條件的重用和擴展性不是很好
  • 第二,上面程序可讀性一般,並不能一目了然知道程序在干嘛

使用Specifications

為了重用查詢條件,我們引入了Specification接口,這是從Eric Evans’ Domain Driven Design 一書中的概念衍生出來的,它為對一個實體查詢的謂詞定義了一個規范,實體類型由Specification接口的泛型參數來決定,這個接口只包含下面一個方法:

public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

我們現在可以通過一個工具類很容易的使用它:

public CustomerSpecifications { public static Specification<Customer> customerHasBirthday() { return new Specification<Customer> { public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) { return cb.equal(root.get(Customer_.birthday), today); } }; } public static Specification<Customer> isLongTermCustomer() { return new Specification<Customer> { public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) { return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2)); } }; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

誠然,這並不是最優雅的代碼,但是至少很好地解決了我們重用判定條件的需求,如何執行呢,很簡單,我們只要讓repository繼承JpaSpecificationExecutor接口即可:

public interface CustomerRepository extends JpaRepository<Customer>, JpaSpecificationExecutor { // Your query methods here }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

然后可以像下面這樣調用:

customerRepository.findAll(hasBirthday());
customerRepository.findAll(isLongTermCustomer());
  • 1
  • 2
  • 1
  • 2

默認實現會為你提供CriteriaQuery,Root,CriteriaBuilder等對象,通過給定的Specification應用判定條件,然后執行查詢,這樣的好處就是我們可以隨意組合查詢條件,而不用寫很多個方法,Specifications工具類提供了一寫遍歷方法來組合條件,例如and(…)、or(…)等連接方法,還有where(…)提供了更易讀的表達形式,下面我們看一下效果:

customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));
  • 1
  • 1

相比JPA Criteria API的原生接口,我們的實現更加具有擴展性和可讀性,當時實現Specification的時候需要一點小波折,但這是值得的

使用Querydsl

為了解決上述的痛苦,一個叫Querydsl的開源項目也提供了類似的解決方案,但是實現有所不同,提供了更有好的API,而且不僅支持JPA,還支持hibernate,JDO,Lucene,JDBC甚至是原始集合的查詢

為了使用Querydsl,需要在pom.xml中引入依賴並且配置一個額外的APT插件

<plugin> <groupId>com.mysema.maven</groupId> <artifactId>maven-apt-plugin</artifactId> <version>1.0</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources</outputDirectory> <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

下面就可以通過QCustomer來實現我們上述的功能了

QCustomer customer = QCustomer.customer;
LocalDate today = new LocalDate(); BooleanExpression customerHasBirthday = customer.birthday.eq(today); BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

上面的寫法不僅讀來很順暢,BooleanExpressions還可以直接重用,免去使用更多包裝方法的寫法,更酷的是還可以得到IDE代碼自動完成的支持,要執行查詢,跟Specification類似,讓repository繼承QueryDslPredicateExecutor接口即可:

public interface CustomerRepository extends JpaRepository<Customer>, QueryDslPredicateExecutor { // Your query methods here }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

可以通過下面的方式調用

BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2)); customerRepository.findAll(customerHasBirthday.and(isLongTermCustomer));
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

總結

Spring Data JPA repository抽象允許通過把JPA Criteria API包裝到Specification中來簡化開發,還可以使用Querydsl,實現方法也很簡單,分別集成JpaSpecificationExecutor或者QueryDslPredicateExecutor即可,當然,如果需要的話,一起使用也沒問題

原文https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM