从零开始 Spring Boot 69:JPA 条件查询
图源:简书 (jianshu.com)
在之前的文章中我们学习过条件查询(Criterial Query),构建条件查询的一般步骤是:
- 获取
HibernateCriteriaBuilder
- 利用
HibernateCriteriaBuilder
创建JpaCriteriaQuery
- 利用
JpaCriteriaQuery
获取查询的根 - 利用
HibernateCriteriaBuilder
构建谓词 - 用谓词组装
JpaCriteriaQuery
- 利用
JpaCriteriaQuery
创建Query
并执行查询
本篇文章将进一步说明构建条件查询的细节问题。
本文的测试用例将使用下面的实体类:
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student {
public enum Level {
FRESH_MAN, SOPHOMORE, JUNIOR, SENIOR
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@NotNull
@Length(max = 45)
@Column(unique = true)
private String name;
@Min(0)
@Max(100)
@NotNull
private Integer averageScore;
@Enumerated(EnumType.STRING)
@NotNull
private Level level;
}
先看一个简单的示例——查找所有的一年级学生:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
JpaPredicate level = cb.equal(root.get("level"), Student.Level.FRESH_MAN);
query = query.where(level);
var students = session.createQuery(query).getResultList();
在这个示例中,用cb.equal
构建了一个类似 JPQL 中的s.level=:level
这样的查询条件,在这里被称作谓词(Predicate),其类型是JpaPredicate
。
谓词可以通过JpaCriteriaQuery.where
方法绑定到条件查询上。
条件语句
可以用一系列谓词(Predicate)构建查询条件,下面会介绍一些常见的条件谓词。
and
可以用多个谓词组合成更复杂的查询条件,比如下面这个:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
JpaPredicate level = cb.equal(root.get("level"), Student.Level.FRESH_MAN);
JpaPredicate averageScore = cb.ge(root.get("averageScore"), 60);
JpaPredicate and = cb.and(level, averageScore);
query = query.where(and);
var students = session.createQuery(query).getResultList();
这里用了三个谓词,组合成了一个类似 JPQL 中的s.level=:level and s.averageScore>=:score
这样的查询条件,用于查询一年级平均分为60分以上的学生。
谓词中的
ge
表示 greater equal(大于等于),gt
表示 greater than(大于),le
和lt
类似。
实际使用中更多的是用级联调用和表达式,并不会为每个谓词创建本地变量,这样可读性会更好:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
query = query.where(cb.and(
cb.in(root.get("level"), List.of(Student.Level.FRESH_MAN, Student.Level.SOPHOMORE)),
cb.ge(root.get("averageScore"), 60))
);
var students = session.createQuery(query).getResultList();
这个示例查询一年级和二年级所有分数大于等于60分的学生。
or
当然,条件语句并非只有一种写法,比如可以用or
来替换in
:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
query = query.where(cb.and(
cb.or(
cb.equal(root.get("level"), Student.Level.FRESH_MAN),
cb.equal(root.get("level"), Student.Level.SOPHOMORE)),
cb.ge(root.get("averageScore"), 60))
);
var students = session.createQuery(query).getResultList();
和上边的示例具有相同的功能。
between
一般如果我们要查询某个区间的结果,可能会这样组合使用谓词:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
query = query.where(cb.and(cb.ge(root.get("averageScore"), 20),
cb.le(root.get("averageScore"), 80)));
var students = session.createQuery(query).getResultList();
JPA 生成的 SQL 语句为:
select s1_0.id,s1_0.average_score,s1_0.level,s1_0.name from student s1_0 where s1_0.average_score>=? and s1_0.average_score<=?
可以使用 between 谓词起到类似的效果:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
query = query.where(cb.between(root.get("averageScore"), 20, 80));
var students = session.createQuery(query).getResultList();
JPA 生成的 SQL 语句:
select s1_0.id,s1_0.average_score,s1_0.level,s1_0.name from student s1_0 where s1_0.average_score between ? and ?
SQL between 操作符的相关介绍说明可以阅读这里。
like
like 也是常用的 SQL 操作符,对应 like 谓词:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Student> query = cb.createQuery(Student.class);
JpaRoot<Student> root = query.from(Student.class);
query = query.where(cb.like(root.get("name"), "B%"));
var students = session.createQuery(query).getResultList();
JPA 生成的 SQL 语句:
select s1_0.id,s1_0.average_score,s1_0.level,s1_0.name from student s1_0 where s1_0.name like replace(?,'\\','\\\\')
in
使用 in 可以检索字段是否在某个范围内:
query = query.where(cb.in(root.get("level"), List.of(Student.Level.FRESH_MAN, Student.Level.SOPHOMORE)));
还有另一种写法:
query = query.where(root.get("level").in(Student.Level.FRESH_MAN, Student.Level.SOPHOMORE));
这种写法更接近 SQL 的风格。
isNull
如果字段可以为null
,就需要用isNull
或isNotNull
创建查询条件:
query = query.where(cb.isNull(root.get("name")));
JPA 生成的 SQL:
select s1_0.id,s1_0.average_score,s1_0.level,s1_0.name from student s1_0 where s1_0.name is null
isNotNull
与之类似,这里不再赘述。
也可以用另一种风格的写法:
query = query.where(root.get("name").isNull());
排序
可以给条件查询添加排序:
query = query.orderBy(cb.desc(root.get("averageScore")),
cb.asc(root.get("name")));
生成的 SQL:
select s1_0.id,s1_0.average_score,s1_0.level,s1_0.name from student s1_0 order by s1_0.average_score desc,s1_0.name
聚合函数
条件语句同样可以使用聚合函数。
比如用count
查询数据条目:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Long> query = cb.createQuery(Long.class);
JpaRoot<Student> root = query.from(Student.class);
query.select(cb.count(root));
Long count = session.createQuery(query).getSingleResult();
System.out.println(count);
注意,这里的检索结果要映射到一个Long
类型的变量,所以cb.createQuery
的参数是Long.class
,而非一般的Student.class
。
JPA 生成的 SQL:
select count(s1_0.id) from student s1_0
JPA 生成的 SQL 符合我们一般的编写 SQL 习惯——对主键字段进行计数。
再比如计算平均分总和:
var cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaQuery<Integer> query = cb.createQuery(Integer.class);
JpaRoot<Student> root = query.from(Student.class);
query.select(cb.sum(root.get("averageScore")));
Integer sum = session.createQuery(query).getSingleResult();
System.out.println(sum);
CriteriaUpdate
可以创建一个CriteriaUpdate
来构建UPDATE
语句,并更新数据:
HibernateCriteriaBuilder cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaUpdate<Student> criteriaUpdate = cb.createCriteriaUpdate(Student.class);
Root<Student> root = criteriaUpdate.from(Student.class);
criteriaUpdate.set("averageScore", 99);
criteriaUpdate.where(cb.equal(root.get("name"), "icexmoon"));
Transaction transaction = session.beginTransaction();
session.createMutationQuery(criteriaUpdate).executeUpdate();
transaction.commit();
JPA 生成的 SQL:
update student set average_score=? where name=?
CriteriaDelete
可以创建一个CriteriaDelete
来构建DELETE
语句,用于删除数据:
HibernateCriteriaBuilder cb = sessionFactory.getCriteriaBuilder();
JpaCriteriaDelete<Student> criteriaDelete = cb.createCriteriaDelete(Student.class);
Root<Student> root = criteriaDelete.from(Student.class);
criteriaDelete.where(cb.le(root.get("averageScore"), 60));
Transaction transaction = session.beginTransaction();
session.createMutationQuery(criteriaDelete).executeUpdate();
transaction.commit();
The End,谢谢阅读。
可以从这里获取本文的完整示例代码。
参考资料
- Combining JPA And/Or Criteria Predicates | Baeldung
- SQL BETWEEN 操作符 | 菜鸟教程 (runoob.com)