Spring-Data-JPA 動態查詢黑科技

在開發中,用到動態查詢的地方,所有的查詢條件包括分頁參數,都會被封裝成一個查詢類XxxQuery

比如說上一篇中的Item

那麼ItemQuery就像這樣

@Data
public class ItemQuery {
    private Integer itemId;//id精確查詢 =
    private String itemName;//name模糊查詢 like
  	//價格查詢
    private Integer itemPrice;// 價格小於'條件' <
}

那現在問題來了,如何去標識這些字段該用怎樣的查詢條件連接呢,還要考慮到每個查詢類都可以通用.


可以用字段註解,來標識字段的查詢連接條件

//用枚舉類表示查詢連接條件
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
    ;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface QueryWord {
    // 數據庫中字段名,默認爲空字符串,則Query類中的字段要與數據庫中字段一致
    String column() default "";
    // equal, like, gt, lt...
    MatchType func() default MatchType.equal;
    // object是否可以爲null
    boolean nullable() default false;
    // 字符串是否可爲空
    boolean emptiable() default false;
}

好了,現在我們可以改造一下ItemQuery

@Data
public class ItemQuery {
    @QueryWord(column = "item_id", func = MatchType.equal)
    private Integer itemId;
    @QueryWord(func = MatchType.like)
    private String itemName;
  
    @QueryWord(func = MatchType.le)
    private Integer itemPrice;
}

現在,我們還需要去構造出查詢時的動態條件,那就創建一個所有查詢類的基類BaseQuery,我們把分頁的條件字段放在基類裏.

/**
 * 所有查詢類的基類
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
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
    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註解
                QueryWord qw = field.getAnnotation(QueryWord.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.emptiable())
                            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;
    }
}

BaseQuery裏,就通過toSpecWithAnd() toSpecWithOr()方法動態構建出了查詢條件.

那現在ItemQuery就要繼承BaseQuery,並實現toSpec()抽象方法

@Data
public class ItemQuery extends BaseQuery<Item> {
    @QueryWord(column = "item_id", func = MatchType.equal)
    private Integer itemId;
    @QueryWord(func = MatchType.like)
    private String itemName;
  
    @QueryWord(func = MatchType.le)
    private Integer itemPrice;
    @Override
    public Specification<Item> toSpec() {
        return super.toSpecWithAnd();//所有條件用and連接
    }
}

當然肯定還有其他不能在BaseQuery中構建的查詢條件,那就在子類的toSpec()實現中添加,

比如下面的例子,ItemQuery條件改成這樣

@QueryWord(column = "item_id", func = MatchType.equal)
private Integer itemId;
@QueryWord(func = MatchType.like)
private String itemName;
//價格範圍查詢
private Integer itemPriceMin;
private Integer itemPriceMax;

那其他條件就可以在toSpec()添加,這樣就可以很靈活的構建查詢條件了

@Override
public Specification<Item> toSpec() {
    Specification<Item> spec = super.toSpecWithAnd();
    return ((root, criteriaQuery, criteriaBuilder) -> {
        List<Predicate> predicatesList = new ArrayList<>();
        predicatesList.add(spec.toPredicate(root, criteriaQuery, criteriaBuilder));
        if (itemPriceMin != null) {
            predicatesList.add(
                    criteriaBuilder.and(
                            criteriaBuilder.ge(
                                    root.get(Item_.itemPrice), itemPriceMin)));
        }
        if (itemPriceMax != null) {
            predicatesList.add(
                    criteriaBuilder.and(
                            criteriaBuilder.le(
                                    root.get(Item_.itemPrice), itemPriceMax)));
        }
       return criteriaBuilder.and(predicatesList.toArray(new Predicate[predicatesList.size()]));
    });
}

調用:

@Test
public void test1() throws Exception {
    ItemQuery itemQuery = new ItemQuery();
    itemQuery.setItemName("車");
    itemQuery.setItemPriceMax(50);
    itemQuery.setItemPriceMax(200);
    Pageable pageable = itemQuery.toPageable(new Sort(Sort.Direction.ASC, "itemId"));
    Page<Item> all = itemRepository.findAll(itemQuery.toSpec(), pageable);
}

現在這個BaseQueryQuertWord就可以在各個動態查詢處使用了,只需在查詢字段上標註@QueryWord註解,

然後實現BaseQuery中的抽象方法toSpec(),通過JpaSpecificationExecutor接口中的這幾個方法,就可以實現動態查詢了,是不是很方便.

public interface JpaSpecificationExecutor<T> {
    T findOne(Specification<T> var1);
    List<T> findAll(Specification<T> var1);
    Page<T> findAll(Specification<T> var1, Pageable var2);
    List<T> findAll(Specification<T> var1, Sort var2);
    long count(Specification<T> var1);
}
發佈了62 篇原創文章 · 獲贊 28 · 訪問量 2131
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章