利用JPA的Specification<T>接口和元模型就實現動態查詢了。但是這樣每一個需要動態查詢的地方都需要寫一個這樣類似的findByConditions方法,小型項目還好,大型項目中其實會造成人力資源的浪費,進行了大量的重復工作,所以想着對動態查詢進行封裝,使其使用起來更加方便。
在開發中,用到動態查詢的地方,所有的查詢條件包括分頁參數,都會被封裝成一個查詢類XxxQuery,我們封裝的思路是創建一個BaseQuery類,在其中實現動態查詢的封裝,即提供幾個模板方法,將查詢類的所有屬性按照連接規則,拼裝成一個Specification型的對象返回,那么問題來了,如何去標識這些字段該用怎樣的查詢條件連接呢,還要考慮到每個查詢類都可以通用,可以用字段注解,來標識字段的查詢連接條件。
創建枚舉類MatchType,列出所有的連接條件
package powerx.io; public enum MatchType { equal, // filed = value //下面四個用於Number類型的比較 gt, // filed > value ge, // field >= value lt, // field < value le, // field <= value notEqual, // field != value like, // field like value notLike, // field not like value // 下面四個用於可比較類型(Comparable)的比較 greaterThan, // field > value greaterThanOrEqualTo, // field >= value lessThan, // field < value lessThanOrEqualTo // field <= value }
自定義注解,用來標識字段
package powerx.io; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented public @interface QueryCondition { // 數據庫中字段名,默認為空字符串,則Query類中的字段要與數據庫中字段一致 String column() default ""; // equal, like, gt, lt... MatchType func() default MatchType.equal; // object是否可以為null boolean nullable() default false; // 字符串是否可為空 boolean emptyable() default false; }
改造查詢實體
package powerx.io.bean; import org.springframework.data.jpa.domain.Specification; import powerx.io.MatchType; import powerx.io.QueryCondition; public class ProductQuery { @QueryCondition(func=MatchType.like) private String name; @QueryCondition(func=MatchType.le) private Double price; public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } }
下面是最核心的部分,公共查詢實體類,其中在toSpecWithLogicType方法中利用反射機制,將所有的屬性按照注解的規則加入到動態查詢條件中
package powerx.io.bean; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import powerx.io.QueryCondition; public abstract class BaseQuery<T> { // start from 0 protected int pageIndex = 0; protected int pageSize = 10; /** * 將查詢轉換成Specification * @return */ public abstract Specification<T> toSpec(); //JPA分頁查詢類 public Pageable toPageable() { return new PageRequest(pageIndex, pageSize); } //JPA分頁查詢類,帶排序條件 public Pageable toPageable(Sort sort) { return new PageRequest(pageIndex, pageSize, sort); } //動態查詢and連接 protected Specification<T> toSpecWithAnd() { return this.toSpecWithLogicType("and"); } //動態查詢or連接 protected Specification<T> toSpecWithOr() { return this.toSpecWithLogicType("or"); } //logicType or/and @SuppressWarnings({ "rawtypes", "unchecked" }) private Specification<T> toSpecWithLogicType(String logicType) { BaseQuery outerThis = this; return (root, criteriaQuery, cb) -> { Class clazz = outerThis.getClass(); //獲取查詢類Query的所有字段,包括父類字段 List<Field> fields = getAllFieldsWithRoot(clazz); List<Predicate> predicates = new ArrayList<>(fields.size()); for (Field field : fields) { //獲取字段上的@QueryWord注解 QueryCondition qw = field.getAnnotation(QueryCondition.class); if (qw == null) continue; // 獲取字段名 String column = qw.column(); //如果主注解上colume為默認值"",則以field為准 if (column.equals("")) column = field.getName(); field.setAccessible(true); try { // nullable Object value = field.get(outerThis); //如果值為null,注解未標注nullable,跳過 if (value == null && !qw.nullable()) continue; // can be empty if (value != null && String.class.isAssignableFrom(value.getClass())) { String s = (String) value; //如果值為"",且注解未標注emptyable,跳過 if (s.equals("") && !qw.emptyable()) continue; } //通過注解上func屬性,構建路徑表達式 Path path = root.get(column); switch (qw.func()) { case equal: predicates.add(cb.equal(path, value)); break; case like: predicates.add(cb.like(path, "%" + value + "%")); break; case gt: predicates.add(cb.gt(path, (Number) value)); break; case lt: predicates.add(cb.lt(path, (Number) value)); break; case ge: predicates.add(cb.ge(path, (Number) value)); break; case le: predicates.add(cb.le(path, (Number) value)); break; case notEqual: predicates.add(cb.notEqual(path, value)); break; case notLike: predicates.add(cb.notLike(path, "%" + value + "%")); break; case greaterThan: predicates.add(cb.greaterThan(path, (Comparable) value)); break; case greaterThanOrEqualTo: predicates.add(cb.greaterThanOrEqualTo(path, (Comparable) value)); break; case lessThan: predicates.add(cb.lessThan(path, (Comparable) value)); break; case lessThanOrEqualTo: predicates.add(cb.lessThanOrEqualTo(path, (Comparable) value)); break; } } catch (Exception e) { continue; } } Predicate p = null; if (logicType == null || logicType.equals("") || logicType.equals("and")) { p = cb.and(predicates.toArray(new Predicate[predicates.size()]));//and連接 } else if (logicType.equals("or")) { p = cb.or(predicates.toArray(new Predicate[predicates.size()]));//or連接 } return p; }; } //獲取類clazz的所有Field,包括其父類的Field private List<Field> getAllFieldsWithRoot(Class<?> clazz) { List<Field> fieldList = new ArrayList<>(); Field[] dFields = clazz.getDeclaredFields();//獲取本類所有字段 if (null != dFields && dFields.length > 0) fieldList.addAll(Arrays.asList(dFields)); // 若父類是Object,則直接返回當前Field列表 Class<?> superClass = clazz.getSuperclass(); if (superClass == Object.class) return Arrays.asList(dFields); // 遞歸查詢父類的field列表 List<Field> superFields = getAllFieldsWithRoot(superClass); if (null != superFields && !superFields.isEmpty()) { superFields.stream(). filter(field -> !fieldList.contains(field)).//不重復字段 forEach(field -> fieldList.add(field)); } return fieldList; } public int getPageIndex() { return pageIndex; } public void setPageIndex(int pageIndex) { this.pageIndex = pageIndex; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } }
在BaseQuery里,就通過toSpecWithAnd() toSpecWithOr()方法動態構建出了查詢條件。那現在ItemQuery就要繼承BaseQuery,並實現toSpec()抽象方法
package powerx.io.bean; import org.springframework.data.jpa.domain.Specification; import powerx.io.MatchType; import powerx.io.QueryCondition; public class ProductQuery extends BaseQuery<Product>{ @QueryCondition(func=MatchType.like) private String name; @QueryCondition(func=MatchType.le) private Double price; public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public Specification<Product> toSpec() { return super.toSpecWithAnd(); } }
當然肯定還有其他不能在BaseQuery中構建的查詢條件,比如說查詢價格在某個區間的情況,那就在子類的toSpec()實現中添加
@Override public Specification<Product> toSpec() { Specification<Product> spec = super.toSpecWithAnd(); return ((root, criteriaQuery, criteriaBuilder) -> { List<Predicate> predicatesList = new ArrayList<>(); predicatesList.add(spec.toPredicate(root, criteriaQuery, criteriaBuilder)); if (priceMin != null) { predicatesList.add( criteriaBuilder.and( criteriaBuilder.ge( root.get(Product_.price), priceMin))); } if (priceMax != null) { predicatesList.add( criteriaBuilder.and( criteriaBuilder.le( root.get(Product_.price), priceMax))); } return criteriaBuilder.and(predicatesList.toArray(new Predicate[predicatesList.size()])); }); }
調用代碼
public Page<Product> findByConditions3(String name, Double price) { ProductQuery pq = new ProductQuery(); pq.setName(name); pq.setPrice(price); return productDao.findAll(pq.toSpec(), pq.toPageable()); }
響應如下:

現在這個BaseQuery和QuertWord就可以在各個動態查詢處使用了,只需在查詢字段上標注@QueryWord注解。然后實現BaseQuery中的抽象方法toSpec(),通過JpaSpecificationExecutor接口中的這幾個方法,就可以實現動態查詢了。
