在 JPA 中使用 CriteriaQuery 执行 COUNT 查询

1、简介

Java Persistence API(JPA)是一种广泛使用的规范,用于访问、持久化和管理 Java 对象与关系数据库之间的数据。JPA 应用中的一项常见任务是计算符合特定条件的实体数量。使用 JPA 提供的 CriteriaQuery API 可以高效地完成这项任务。

CriteriaQuery 的核心组件是 CriteriaBuilderCriteriaQuery 接口。CriteriaBuilder 是创建各种查询元素(如 Predicate、表达式和 CriteriaQuery)的工厂。而,CriteriaQuery 代表一个查询对象,它封装了 selectfilterorder 标准。

本文将带你了解 JPA 中的 COUNT 查询,学习如何利用 CriteriaQuery API 轻松高效地执行 COUNT 操作。

本文以一个简单的图书管理系统为例,介绍如何利用 CriteriaQuery API 生成各种场景下的图书 COUNT 查询。

2、依赖

创建示例项目。

添加所需的 maven 依赖,包括 spring-data-jpaspring-boot-starter-testh2 内存数据库:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

依赖添加完成后,创建图书管理系统示例。它允许我们执行各种查询,如统计所有图书,统计某个作者、书名和年份的图书的各种组合。

添加一个 Book (图书)实体,包含了 titleauthorcategoryyear 字段:

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String Category;
    private String author;
    private int year;

    // 构造函数、Getter/Setter 方法省略
}

创建其 repository 接口,可以对 Book 实体进行各种操作:

public interface BookRepositoryCustom {
    long countAllBooks();
    long countBooksByTitle(String title);
    long countBooksByAuthor(String author);
    long countBooksByCategory(String category);
    long countBooksByTitleAndAuthor(String title, String author);
    long countBooksByAuthorOrYear(String author, int year);
}

3、使用 CriteriaQuery 计算实体数量

COUNT 查询通常用于确定满足特定条件的实体总数。使用 CriteriaQuery,我们可以直接、高效地构建 COUNT 查询。

3.1、初始化 CriteriaBuilder 和 CriteriaQuery

要构建 COUNT 查询,首先需要从 EntityManager 获取 CriteriaBuilder 的实例。

CriteriaBuilder 是创建查询元素的入口点:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

该对象用于构建查询的不同部分,如条件查询、表达式、Predicate 和 select。接下来,用它创建一个 CriteriaQuery:

CriteriaQuery<Long> cq = cb.createQuery(Long.class);

如上,将查询的结果类型指定为 Long,表示希望查询返回一个 count 值。

3.2、创建 Root 和 Count 表达式

接下来,创建一个 Root 对象,代表要执行 count 操作的实体。然后,使用 CriteriaBuilder 根据该根对象构建一个 count 表达式:

Root<Book> bookRoot = cq.from(Book.class);
cq.select(cb.count(bookRoot));

如上,首先定义查询的 root 对象,指定查询基于 Book 实体。接下来,使用 CriteriaBuilder 提供的 cb.count() 创建一个 count 表达式。count 方法计算查询结果中的行数。它将表达式(本例中为 bookRoot)作为参数,并返回一个表达式,该表达式表示符合查询中定义的条件的行数。

最后,cq.select() 会将查询结果设置为这个 count 表达式。本质上,它告诉查询,最终结果应该是符合指定条件的 Book 实体的数量。

3.3、执行查询

构建好了 CriteriaQuery 后,就可以使用 EntityManager 执行查询:

Long count = entityManager.createQuery(cq).getSingleResult();

如上,使用 entityManager.createQuery(cq)CriteriaQuery 创建一个 TypedQuery,并使用 getSingleResult() 以单个结果的形式检 COUNT 值。

4、处理 Criteria 和条件

在实际中,COUNT 查询经常需要根据某些 Criteria 或条件进行过滤。Criteria 查询提供了一种灵活的机制,可使用 Predicate 为查询添加 Criteria。

接下来看看如何利用多重条件来生成 COUNT 查询。假设我们想查询标题中包含特定关键词的所有书籍的数量:

long countBooksByTitle(String titleKeyword) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<Book> bookRoot = cq.from(Book.class);
    Predicate condition = cb.like(bookRoot.get("title"), "%" +    titleKeyword + "%");
    cq.where(condition);
    cq.select(cb.count(bookRoot));
    return entityManager.createQuery(cq).getSingleResult()
}

除了前面的步骤,还为 COUNT 查询创建了一个 Predicate,代表 SQL 查询的 WHERE 子句。

cb.like 方法会创建一个条件,检查书名是否包含 titleKeyword% 是通配符,可以匹配任何字符序列。然后,使用 cq.where(condition) 将此 Predicate 添加到 CriteriaQuery 中,从而将此条件应用到查询中。

另一个场景是获取某个作者所有书籍的数量:

long countBooksByAuthor(String authorName) {        
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<Book> bookRoot = cq.from(Book.class);
    Predicate condition = cb.equal(bookRoot.get("author"), authorName);
    cq.where(condition);
    cq.select(cb.count(bookRoot));
    return entityManager.createQuery(cq).getSingleResult();
}

如上,Predicate 基于 cb.equal() 方法,该方法只过滤包含确切 authorName 的记录。

5、组合多个 Criteria

Criteria 查询允许我们使用 ANDORNOT 等逻辑运算符组合多个 Criteria

考虑一下根据多个条件计算图书数量的情况,假设,我们想获得包含特定作者、书名和出版年份的所有图书数量:

long countBooksByAuthorOrYear(int publishYear, String authorName) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<Book> bookRoot = cq.from(Book.class);
    Predicate authorCondition = cb.equal(bookRoot.get("author"), authorName);
    Predicate yearCondition = cb.greaterThanOrEqualTo(bookRoot.get("publishYear"), 1800);
    cq.where(cb.or(authorCondition, yearCondition));
    cq.select(cb.count(bookRoot));
    return entityManager.createQuery(cq).getSingleResult();
}

如上,创建了两个 Predicate,分别代表图书作者和出版年份的条件。然后,使用 cb.and() 将这些谓词组合起来,形成一个复合条件。

同样,也可以在某种情况下获取具有特定书名或具有作者和年份组合的图书的数量:

long countBooksByTitleOrYearAndAuthor(String authorName, int publishYear, String titleKeyword) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<Book> bookRoot = cq.from(Book.class);

    Predicate authorCondition = cb.equal(bookRoot.get("author"), authorName);
    Predicate yearCondition = cb.equal(bookRoot.get("publishYear"), publishYear);
    Predicate titleCondition = cb.like(bookRoot.get("title"), "%" + titleKeyword + "%");

    Predicate authorAndYear = cb.and(authorCondition, yearCondition);
    cq.where(cb.or(authorAndYear, titleCondition));
    cq.select(cb.count(bookRoot));

    return entityManager.createQuery(cq).getSingleResult();
}

如上,再次创建了三个 Predicate,但在 authorAndYearCondition Predicate 和 titleCondition Predicate 之间使用 cb.or(authorAndYear, titleCondition) 进行或运算。

6、集成测试

现在,使用 Spring 提供的 @DataJPATest 注解,在测试中注入必要的 Repository 层,将内存数据库中的 H2 用作底层持久化存储。在测试类中注入 TestEntityManager,并用它来插入数据。

以获取某个作者的所有书籍的数量为例:

@Test
void givenBookDataAdded_whenCountBooksByAuthor_thenReturnsCount() {
    entityManager.persist(new Book("Java Book 1", "Author 1", 1967, "Non Fiction"));
    entityManager.persist(new Book("Java Book 2", "Author 1", 1999, "Non Fiction"));
    entityManager.persist(new Book("Spring Book", "Author 2", 2007, "Non Fiction"));

    long count = bookRepository.countBooksByAuthor("Author 1");

    assertEquals(2, count);
}

与上例类似,我们可以为 repository 中提供的所有不同 COUNT 查询场景编写测试。

7、总结

本文介绍了如何在 Spring Boot 应用中使用 JPA Criteria API 执行 COUNT 查询。


Ref:https://www.baeldung.com/jpa-criteriaquery-count-queries