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);
}

原文链接:Spring-Data-JPA 动态查询黑科技 | 火尧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,358评论 6 343
  • 1.简单的谈一下SpringMVC的工作流程? 流程1、用户发送请求至前端控制器DispatcherServlet...
    kerb阅读 198评论 0 0
  • 英国作家查尔斯·狄更斯所著的历史小说《双城记》中有一句话是这样说的“这是最好的时代,也是最坏的时代。”北京是我这...
    飞鸟52赫兹阅读 887评论 0 1