Spring中CriteriaBuilder.In的使用

.in方法的用途

Criteria意为“标准、准则”,在数据库中翻译为“查询条件”,所以CriteriaBuider就是Java提供的、用来生成查询条件的“标准生成器”。

Criteria的in方法对应SOL语句中的IN关键字。
比如,

// 在student表中查询所有班级id为1或2的学生
SELECT * FROM `student` WHERE klass_id IN (1, 2);

因此in的作用就是:判断一条记录的某个值,是否在给定的数组里面。
上面的代码中,给定了1,2两个班级,那么在所有学生中,只要学生所在的班级是1或2中的任何一个,这个学生都会被查出来。

.in的用法

baeldung中有比较详细的教程,但原文还是不够通俗。
接下来我把文章翻译为中文,把代码处理的更易懂一些。

in( )方法接受一个Expression并返回CriteriaBuilder.In类型的新 Predicate。 它可用于测试给定表达式是否包含在值列表中。
/**
 * 单词解释:
 * criteria: 标准
 * criteriaQuary: 标准查询
 * criteriaBuider: 标准生成器
 * clause: 规则、法则
 *
 * @Input 一个课程数组klasses
 */
 
// 构建一个标准查询,这个criteriaQuery是查询主体,用它来发起“查询”这个动作
CriteriaQuery criteriaQuery = 
  criteriaBuilder.createQuery(Student.class);
  
// root是需要查询的表,代码中的root基于Student,所以指的是“学生”表
Root root = criteriaQuery.from(Student.class);

// 使用buider构建一个in的查询规则,in()的参数是被查询的对象中的某个属性(比如学生的班级)
// 稍后作比较时,就用这个属性(学生的班级)和数组(传入的一组班级)做对比
In inClause = criteriaBuilder.in(root.get("klass"));

// klasses是传入的数组,通过循环,把数组中的每个值依次添加到in的查询规则中
// 比如,klasses有1,2两个班,那么in查询规则就包含这两个班
for (String klass : klasses) {
    inClause.value(klass);
}

// 通过“标准查询”,对学生表,发起“查询”操作,查询条件为in查询规则
criteriaQuery.select(root).where(inClause);

到此就明白了in的基本用法,然而,项目使用的是JPA,而且已经有了其他查询条件,如何把这种原生查询的写法,改写成适用于JPA的写法,从而拼接到已有的代码中,就是个问题,因此我继续探究。

Spring Data JPA中的specification

在Java后端的综合查询中,我们一定会经常与JPA打交道,比如Spring Data JPA

在之前学习的时候,在教程的4.6.3 综合查询中就学习了如何在Repository中进行条件查询。

specification译为规范、说明书。
在Repository中,我们只需要写好Specification,然后传到仓库的方法中,仓库就会按照传入的的查询条件进行查询。

例如,先用CriteriaBuider写一个查询条件:

/**
 * 学生查询条件
 */
public class StudentSpecs {
    /**
     * 属于某个班级
     * @param klass 班级
     */
    public static Specification belongToKlass(Klass klass) {
        return (Specification) (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("klass").as(Klass.class), klass);
    }
}

然后在仓库层findAll方法中加入这个查询条件:

    default Page findAll(Klass klass, @NotNull Pageable pageable) {
        Specification specification = StudentSpecs.belongToKlass(klass);
        return this.findAll(specification, pageable);
    }

这种方式与上面讲到的原生查询方式相比,优势在于剥离了查询的“方法”和其中的查询“条件”:

  • 定义查询条件时,只需要单独维护、测试这一个条件即可
  • 实际查询时,可以把现有的查询条件灵活组合

区别

通过对比,原生的查询方法,是由标准查询criteriaQuery发起的查询操作。
而在JPA中,我们通过标准构造器criteriaBuilder生成查询条件Specification,然后传给仓库即可,这是写法上最明显的区别。

// 生成查询条件的语句
return (Specification) (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("klass").as(Klass.class), klass);

在上述代码中,返回值类型是Specification查询条件,这里相当于写了一个匿名函数,传入了原生查询中的三个重要参数:查询表名root、标准查询criteriaQuery、标准构造器criteriaBuilder。
匿名函数调用了.equal方法,用来判断第一个参数第二个参数是否相等,方法介绍如下:

javax.persistence.criteria.Predicate equal(javax.persistence.criteria.Expression expression, javax.persistence.criteria.Expression expression1);

in方法在Spring Data JPA中的特殊用法

探究过程(可略过)

由于已经学过.equal方法,因此很容易产生惯性思维:.equal方法是两个参数,方法的作用是比较参数一和参数二。
那么我猜:.in方法应该也是传入两个参数,一个Expression表达式和一个List数组,只要表达式的对象属于后面的数组,就把这条记录查询出来。

事实上我猜错了:.in方法只有一个参数

 javax.persistence.criteria.CriteriaBuilder.In in(javax.persistence.criteria.Expression expression);

因此我陷入了懵B,这和剧本不太一样啊,一个参数怎么比较呢?

最后在老师的指点下,终于找到了答案。

用法

先回来看原生查询中的用法:

  1. 新建标准查询query
  2. 用标准查询创建root
  3. 用buider构建查询规则clause
  4. 用query调用clause完成查询

对比JPA中的用法

  1. 新建查询条件Specification
  2. 执行匿名函数,传入原生查询中的三个参数
  3. 直接返回用buider构建的查询条件

所以理解用法的关键在于明白这一点:JPA查询中返回的内容,实质上就是原生查询中的clause!!

接下来再把.equal和.in对比:

// 生成.equal查询条件的语句
return (Specification) (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("klass").as(Klass.class), klass);

.equal在项目中是直接return了一个匿名函数,如果想把.in也写成这种写法,如何操作呢?

因为.in需要手动向数组添加值,因此无法一步完成,所以首先把匿名箭头函数变成花括号的形式:

// 生成.equal查询条件的语句
return (Specification) (root, criteriaQuery, criteriaBuilder) -> {
    // todo 方法内容
};

接下来原封不动的把Bealdung的示例代码复制进去:

// 生成.equal查询条件的语句
return (Specification) (root, criteriaQuery, criteriaBuilder) -> {
    // 构建一个标准查询,这个criteriaQuery是查询主体,用它来发起“查询”这个动作
    CriteriaQuery criteriaQuery = 
      criteriaBuilder.createQuery(Student.class);

    // root是需要查询的表,代码中的root基于Student,所以指的是“学生”表
    Root root = criteriaQuery.from(Student.class);

    // 使用buider构建一个in的查询规则,in()的参数是被查询的对象中的某个属性(比如学生的班级)
    // 稍后作比较时,就用这个属性(学生的班级)和数组(传入的一组班级)做对比
    In inClause = criteriaBuilder.in(root.get("klass"));

    // klasses是传入的数组,通过循环,把数组中的每个值依次添加到in的查询规则中
    // 比如,klasses有1,2两个班,那么in查询规则就包含这两个班
    for (String klass : klasses) {
        inClause.value(klass);
    }

    // 通过“标准查询”,对学生表,发起“查询”操作,查询条件为in查询规则
    criteriaQuery.select(root).where(inClause);
};

这时候必然会报错(返回值类型错误),然后再运用刚才的理论:不再使用query完成查询,而是直接把clause返回,从而可以传递给仓库来处理:

// 生成.equal查询条件的语句
return (Specification) (root, criteriaQuery, criteriaBuilder) {
    // 构建一个标准查询,这个criteriaQuery是查询主体,用它来发起“查询”这个动作
    CriteriaQuery criteriaQuery = 
      criteriaBuilder.createQuery(Student.class);

    // root是需要查询的表,代码中的root基于Student,所以指的是“学生”表
    Root root = criteriaQuery.from(Student.class);

    // 使用buider构建一个in的查询规则,in()的参数是被查询的对象中的某个属性(比如学生的班级)
    // 稍后作比较时,就用这个属性(学生的班级)和数组(传入的一组班级)做对比
    In inClause = criteriaBuilder.in(root.get("klass"));

    // klasses是传入的数组,通过循环,把数组中的每个值依次添加到in的查询规则中
    // 比如,klasses有1,2两个班,那么in查询规则就包含这两个班
    for (String klass : klasses) {
        inClause.value(klass);
    }

    // 直接返回查询规则即可
    return inClause;
};

报错消失,为什么呢?因为JPA的Specification和criteriaBuilder生成的Clause本质上就是一个东西,所以返回值类型是兼容的。

到此就解决了SpringDataJPA使用criteriaQuery.in的写法不同。

版权声明

本文作者: 河北工业大学梦云智开发团队 - 刘宇轩
新人经验不足,有建议欢迎交流,有意见欢迎轻喷

你可能感兴趣的:(criteria查询,in,specifications,jpa,java)