1. 引言

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

Criteria Query的核心组件是CriteriaBuilderCriteriaQuery接口CriteriaBuilder是创建各种查询元素(如谓词、表达式和条件查询)的工厂。CriteriaQuery则代表封装了选择、过滤和排序条件的查询对象

本文将深入探讨JPA中的计数查询,展示如何利用Criteria Query API轻松高效地执行计数操作。我们将快速概述CriteriaQuery及其生成计数查询的方法,然后通过图书馆管理系统的实际案例,演示如何在不同场景下使用CriteriaQuery API统计图书数量。

2. 依赖配置与示例搭建

首先确保添加必要的Maven依赖,包括*spring-data-jpaspring-boot-starter-test*和作为内存数据库h2

<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>

依赖配置完成后,我们构建一个简单的图书馆管理系统示例。该系统支持各种查询操作,如统计所有图书、按特定作者/标题/年份统计以及组合条件统计。首先定义包含titleauthorcategoryyear字段的Book实体:

@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
}

接下来创建仓库接口,用于执行针对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统计实体数量

计数查询常用于确定满足特定条件的实体总数。使用CriteriaQuery可以简单高效地构建计数查询。下面分步说明如何在JPA中执行计数查询。

3.1. 初始化CriteriaBuilderCriteriaQuery

构建计数查询的第一步是从*EntityManager获取CriteriaBuilder*实例。该对象是创建查询元素的入口点

CriteriaBuilder cb = entityManager.getCriteriaBuilder();

该对象用于构建查询的各个部分,包括条件查询、表达式、谓词和选择项。接下来创建条件查询:

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

这里指定查询结果类型为Long,表明查询将返回计数值

3.2. 创建Root并选择计数

接下来创建Root对象,表示要执行计数操作的实体。然后使用CriteriaBuilder基于该对象构建计数表达式:

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

首先定义查询根,指定查询基于Book实体。然后使用CriteriaBuilder提供的cb.count()创建计数表达式。**count方法统计查询结果中的行数,它接收表达式(此处为bookRoot)作为参数,返回表示匹配条件的行数的表达式**。

最后,cq.select()将查询结果设置为该计数表达式。本质上,这告诉查询最终结果应为匹配指定条件的Book实体数量(条件将在后续部分介绍)。

3.3. 执行查询

CriteriaQuery构建完成后,使用EntityManager执行查询:

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

*这里使用entityManager.createQuery(cq)CriteriaQuery创建TypedQuery,并通过getSingleResult()*获取计数值作为单个结果**。

4. 处理条件与约束

实际场景中,计数查询通常需要基于特定条件进行过滤。Criteria Query提供了使用谓词为查询添加条件的灵活机制

下面深入探讨如何利用多个条件实现计数查询。假设要统计标题包含特定关键词的图书数量:

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

除前述步骤外,条件计数需要创建Predicate表示SQL查询的WHERE子句

cb.like方法创建检查图书标题是否包含关键词的条件。%是匹配任意字符序列的通配符。通过*cq.where(condition)*将谓词添加到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();
}

此处谓词基于*cb.equal()*方法,仅过滤包含精确作者名的记录。

5. 组合多个条件

Criteria Query允许使用AND、OR和NOT等逻辑运算符组合多个条件。考虑以下基于多个条件统计图书数量的场景。假设要统计特定作者、标题和出版年份的图书数量:

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

*这里创建两个分别表示作者和出版年份条件的谓词,然后使用cb.or()*组合这些谓词形成复合条件**。

类似地,可能需要统计满足以下任一条件的图书数量:标题包含关键词,或同时满足作者和年份条件:

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

*我们创建了三个谓词,现在需要使用cb.or(authorAndYear, titleCondition)authorAndYearConditiontitleCondition*之间建立OR关系**。

6. 集成测试

下面提供集成测试模式,确保计数查询正常工作。使用Spring提供的*@DataJPATest注解,在测试中注入必要的仓库层,底层使用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);
}

类似地,可以为仓库中提供的所有计数场景编写测试用例。

7. 结论

本文演示了如何在Spring Boot应用中使用JPA Criteria API执行条件计数。我们搭建了基础的图书馆管理系统,实现了基于特定条件统计图书数量的自定义仓库方法。

本文的完整实现可在GitHub上获取。


原始标题:Count Queries In JPA Using CriteriaQuery | Baeldung