1、JpaSpecificationExecutor
JPA2引入了一個criteria API,我們可以使用它以編程的形式構建查詢。通過編寫criteria,動態生成query語句。JpaSpecificationExecutor是Spring-Data-JPA為我們執行基於JPA criteria API的Specification查詢接口。想要使用該功能,我們自己的Repository接口繼承這個接口就可以了。該接口提供了幾個根據Specification進行查詢的方法。
JpaSpecificationExecutor源碼:
import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.lang.Nullable; /** * 接口,允許執行基於JPA criteria API的Specification查詢。 */ public interface JpaSpecificationExecutor<T> { /** * 返回匹配給定Specification的單個實體,如果沒找到返回 Optional.empty(),如果結果集有多個,拋出IncorrectResultSizeDataAccessException異常 */ Optional<T> findOne(@Nullable Specification<T> spec); /** * 返回匹配給定Specification的所有實體 */ List<T> findAll(@Nullable Specification<T> spec); /** * 返回所有匹配給定Specification的實體並分頁 */ Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable); /** * 返回所有匹配Specification的實體並排序 */ List<T> findAll(@Nullable Specification<T> spec, Sort sort); /** * 返回所有匹配給定Specification的記錄數 */ long count(@Nullable Specification<T> spec); }
2、Specification
JpaSpecificationExecutor的每一個方法中都有一個Specification參數,Specification接口中的toPredicate方法是該接口的核心方法。Specification可以很容易的在實體上構建一組可擴展的Predicate。然后可以對其進行組合使用,這樣JpaRepository就不用為每一種情況都寫一個查詢方法了。
toPredicate方法種有三個參數:
Root<T>,代表查詢和操作實體的根,我們可以通過它的get方法來獲得我們操作的字段,可以寫實體屬性字符串,也可以使用@StaticMetamodel標記的類來指定(后面有示例)。
CriteriaQuery<?>,抽象了整個查詢語句,用來把各個段組合在一起。
CriteriaBuilder,用來構建CritiaQuery的構建器對象,其實就相當於條件或者是條件組合,以謂語即Predicate的形式返回。
Specification源碼:
import static org.springframework.data.jpa.domain.SpecificationComposition.*; import java.io.Serializable; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.springframework.lang.Nullable; /** * 領域驅動設計意義上的規范。 */ public interface Specification<T> extends Serializable { long serialVersionUID = 1L; /** * 否定給定的Specification */ static <T> Specification<T> not(@Nullable Specification<T> spec) { return spec == null // ? (root, query, builder) -> null// : (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder)); } /** * 簡單的靜態工廠方法,給Specification周圍添加一些語法糖 */ @Nullable static <T> Specification<T> where(@Nullable Specification<T> spec) { return spec == null ? (root, query, builder) -> null : spec; } /** * 將給定Specification與當前Specification進行and關聯 */ @Nullable default Specification<T> and(@Nullable Specification<T> other) { return composed(this, other, (builder, left, rhs) -> builder.and(left, rhs)); } /** * 將給定specification與當前specification進行or關聯 */ @Nullable default Specification<T> or(@Nullable Specification<T> other) { return composed(this, other, (builder, left, rhs) -> builder.or(left, rhs)); } /** * 以給定根和CriteriaQuery的謂詞的形式為引用實體的查詢創建WHERE子句。 */ @Nullable Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); }
3、SpecificationComposition
幫助類,以支持specification的組合模式。
import java.io.Serializable; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.springframework.data.jpa.domain.Specification; import org.springframework.lang.Nullable; /** * 幫助類,以支持specification的組合模式 */ class SpecificationComposition { /** * 函數式接口,組合器 */ interface Combiner extends Serializable { Predicate combine(CriteriaBuilder builder, @Nullable Predicate lhs, @Nullable Predicate rhs); } /** * 靜態組合方法,將給定的兩個Specification用指定的組合器進行組合成新的Specification */ @Nullable static <T> Specification<T> composed(@Nullable Specification<T> lhs, @Nullable Specification<T> rhs, Combiner combiner) { return (root, query, builder) -> { Predicate otherPredicate = toPredicate(lhs, root, query, builder); Predicate thisPredicate = toPredicate(rhs, root, query, builder); if (thisPredicate == null) { return otherPredicate; } return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate); }; } /** * 將Specification轉換為Predicate對象 */ private static <T> Predicate toPredicate(Specification<T> specification, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, query, builder); } }
4、使用JPAMetaModelEntityProcessor生成元模型
4.1、導入hibernate-jpamodelgen依賴
<!-- 自動生成元模型 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>5.2.17.Final</version> </dependency>
4.2、IDEA配置Annoation Processors
4.3、和lombok一起使用,添加maven插件(如果兩個同時使用,不添加額外配置的話,lombok不生效)
<!-- JPAMetaModelEntityProcessor與lombok共存 --> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <compilerArguments> <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor</processor> </compilerArguments> </configuration> </plugin>
5、JpaRepositoryImplementation
JpaRepository和JpaSpecificationExecutor接口的子類,我們可以直接繼承改接口。
6、使用示例
6.1、繼承接口
6.2、Specification條件工廠(各條件可以組合使用)
/** * book Specification 條件 * * @author caofanqi */ public class BookSpecs { /** * 書名模糊 */ public static Specification<Book> bookNameLike(String bookName){ return (Specification<Book>) (root, query, builder) -> { //設置抓取策略,解決N+1條SQL問題 root.fetch("category", JoinType.LEFT); return builder.like(root.get("bookName"),"%" + bookName + "%"); }; } /** * 大於前六個月的認為是新書 */ public static Specification<Book> isNewBook(){ return (Specification<Book>) (root, query, builder) -> { LocalDate beforeSixMonth = LocalDate.now().minusMonths(6); root.fetch(Book_.category, JoinType.LEFT); return builder.greaterThan(root.get(Book_.publishDate),beforeSixMonth); }; } /** * 門類名稱模糊 */ public static Specification<Book> categoryNameLike(String categoryName){ return (Specification<Book>) (root, query, builder) -> { root.fetch(Book_.category, JoinType.INNER); return builder.like(root.get(Book_.category).get(Category_.categoryName),"%" + categoryName + "%"); }; } }
6.3、單元測試
@Test void testSpec1(){ List<Book> books = bookRepository.findAll(BookSpecs.bookNameLike("java")); books.forEach(b-> System.out.println(b.getBookName())); } @Test void testSpec2(){ List<Book> books = bookRepository.findAll(BookSpecs.bookNameLike("java").and(BookSpecs.isNewBook())); books.forEach(b-> System.out.println(b.getBookName())); } /** * 排序和分頁一樣使用 */ @Test void testSpec3(){ List<Book> books = bookRepository.findAll(BookSpecs.isNewBook(),Sort.by(Sort.Direction.DESC,"publishDate")); books.forEach(b-> System.out.println(b.getBookName())); } @Test void testSpec4(){ List<Book> books = bookRepository.findAll(BookSpecs.categoryNameLike("數據庫")); books.forEach(b-> System.out.println(b.getBookName())); }
源碼地址:https://github.com/caofanqi/study-spring-data-jpa