spring data jpa 動態查詢(工具類封裝)


  利用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());
    }

  響應如下:

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


免責聲明!

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



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