精通 JPA Criteria API:构建动态、类型安全的企业级查询

在当今的企业级 Java 开发中,数据持久化是核心环节。虽然我们习惯了使用 JPQL 或原生 SQL 来处理数据,但这些基于字符串的查询方式存在一个显著的痛点:编译时无法检查错误。你是否曾因拼写错误导致 SQL 语法报错,或者因为拼接了恶意 SQL 字符串而担忧过安全问题?别担心,JPA Criteria API 正是为了解决这些问题而生的。

在这篇文章中,我们将深入探讨 Criteria API 这一强大的特性。我们将学习如何摆脱繁琐的字符串拼接,以一种完全面向对象、类型安全的方式来构建动态查询。这不仅能让我们写出更健壮的代码,还能在面对复杂的多条件动态查询场景时游刃有余。

为什么我们需要 Criteria API?

在传统的开发中,当我们需要根据用户传入的可选条件(例如:按姓名搜索、按薪资范围过滤、按部门排序)来查询数据时,使用 JPQL 通常意味着我们要写大量的 if-else 语句来拼接字符串,稍有不慎就会出错。

Criteria API 允许我们在运行时通过编程的方式构建查询定义。它最大的优势在于:

  • 类型安全:编译器会在编译阶段检查你的查询逻辑,避免字段名写错等问题。
  • 动态构建:非常适合根据业务逻辑动态生成 WHERE 子句。
  • 面向对象:你操作的是 Java 类的属性,而不是数据库的列名,解耦了数据库层与对象层。

核心概念:构建查询的积木

在我们开始写代码之前,让我们先熟悉一下 Criteria API 中的几个核心“角色”。理解它们是掌握查询构建的关键。

  • CriteriaBuilder (构建器工厂)

这是一切的起点。你可以把它想象成一个“工厂”,通过 EntityManager 获取。它负责创建查询路径、谓词、排序等组件。例如,INLINECODEa9cfc370、INLINECODE1ff3a82e、between 等方法都由它提供。

  • CriteriaQuery (查询定义)

它代表了这次查询的“蓝图”。它不包含数据,只包含查询的逻辑结构:我们要查什么(SELECT)、从哪里查(FROM)、条件是什么(WHERE)。

  • Root (查询根)

这相当于 SQL 语句中的 FROM 子句。它定义了查询的起始实体类型,并让我们能够从这个实体出发去导航到关联的实体。

  • Predicate (谓词/条件)

它代表一个简单的或组合的查询条件,比如 age > 18。一个复杂的 WHERE 子句通常由多个 Predicate 组成。

  • Expression (表达式)

用于定义具体的计算,比如对属性进行加减乘除,或者调用数据库函数。

  • TypedQuery (类型化查询)

当 CriteriaQuery 构建完成后,我们需要执行它。EntityManager 会根据 CriteriaQuery 生成一个 TypedQuery 实例,执行后返回强类型的对象列表,而不是原始的 Object 数组。

实战演练:构建 JPA Criteria API 应用程序

光说不练假把式。让我们通过一个完整的 Maven 项目,从零开始演示如何在实际开发中应用 Criteria API。我们将创建一个员工管理系统,演示如何动态查询员工数据。

#### 步骤 1: 项目初始化

首先,我们需要一个新的 Maven 项目。你可以使用 IntelliJ IDEA 或 Eclipse,具体步骤不再赘述。

  • 项目名称: jpa-criteria-api-demo
  • Java 版本: JDK 11 或更高版本
  • 构建工具: Maven

#### 步骤 2: 引入必要的依赖 (pom.xml)

我们将使用 Hibernate 作为 JPA 的实现,并使用 MySQL 数据库。打开你的 pom.xml 文件,添加以下依赖。请确保版本与你的环境兼容。


    
    
        org.hibernate.orm
        hibernate-core
        6.0.2.Final
    

    
    
        org.glassfish.jaxb
        jaxb-runtime
        3.0.2
    

    
    
        mysql
        mysql-connector-java
        8.0.28
    

    
    
        org.junit.jupiter
        junit-jupiter-api
        5.9.2
        test
    
    
        org.junit.jupiter
        junit-jupiter-engine
        5.9.2
        test
    

#### 步骤 3: 配置数据库连接 (persistence.xml)

在 INLINECODE8ef2136a 目录下创建 INLINECODEbe99c464 文件。这是 JPA 的配置中心,我们将在这里定义数据库连接信息和持久化单元。



    
    
        
        model.Employee

        
            
            
            
            
            
            
            
            
            
            
            
            
        
    

#### 步骤 4: 定义实体模型 (Employee)

创建一个简单的 Employee 实体类。为了保持代码简洁,我们将所有字段都放在主类中,但在实际项目中,建议将 ID 抽取到基类中。

package model;

import jakarta.persistence.*;

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String department;
    private double salary;

    // JPA 要求必须有一个无参构造函数
    protected Employee() {}

    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    // Getter 和 Setter 方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }

    @Override
    public String toString() {
        return "Employee{id=" + id + ", name=‘" + name + "‘, salary=" + salary + "}";
    }
}

#### 步骤 5: 核心逻辑 – 使用 Criteria API 查询

这是最精彩的部分。让我们创建一个类来演示三种常见的查询场景:

  • 简单条件查询:查找薪资大于某个值的员工。
  • 多条件动态查询:根据部门和薪资动态过滤。
  • 模糊匹配与聚合:统计部门的平均薪资。

创建 App.java

import jakarta.persistence.*;
import jakarta.persistence.criteria.*;
import model.Employee;
import java.util.List;

public class App {
    private static EntityManagerFactory entityManagerFactory;

    public static void main(String[] args) {
        // 1. 初始化 EntityManagerFactory
        entityManagerFactory = Persistence.createEntityManagerFactory("example-unit");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        // 简单的数据初始化,为了演示效果
        initSampleData(entityManager);

        System.out.println("
--- 案例 1: 查找薪资高于 60000 的员工 ---");
        findEmployeesBySalary(entityManager, 60000.0);

        System.out.println("
--- 案例 2: 动态多条件查询 (部门 = ‘IT‘ 且 薪资 > 50000) ---");
        findEmployeesByDeptAndSalary(entityManager, "IT", 50000.0);

        System.out.println("
--- 案例 3: 统计部门的平均薪资 ---");
        calculateAvgSalaryByDept(entityManager, "Sales");

        entityManager.close();
        entityManagerFactory.close();
    }

    /**
     * 案例 1: 简单条件查询
     * 演示基本的 Root, Predicate 和 TypedQuery 使用
     */
    public static void findEmployeesBySalary(EntityManager em, Double minSalary) {
        // 1. 获取 CriteriaBuilder
        CriteriaBuilder cb = em.getCriteriaBuilder();

        // 2. 创建 CriteriaQuery,指定返回结果类型为 Employee
        CriteriaQuery query = cb.createQuery(Employee.class);

        // 3. 定义 FROM 子句,设置查询根
        Root root = query.from(Employee.class);

        // 4. 构建 WHERE 条件: salary > minSalary
        // cb.gt 代表 "Greater Than" (大于)
        Predicate salaryCondition = cb.gt(root.get("salary"), minSalary);

        // 5. 将条件添加到查询对象
        query.where(salaryCondition);

        // 6. 执行查询并获取结果
        TypedQuery typedQuery = em.createQuery(query);
        List results = typedQuery.getResultList();

        results.forEach(System.out::println);
    }

    /**
     * 案例 2: 动态多条件查询 (这才是 Criteria API 的杀手锏)
     * 模拟真实场景:前端传入了查询条件,我们不知道用户是否会提供部门或薪资,
     * 但我们可以动态构建查询,而不需要拼接 SQL 字符串。
     */
    public static void findEmployeesByDeptAndSalary(EntityManager em, String deptName, Double minSalary) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery query = cb.createQuery(Employee.class);
        Root root = query.from(Employee.class);

        // 构建 Predicate 列表
        Predicate conditions = cb.conjunction(); // 初始化一个 AND 连接的起点

        // 如果部门不为空,则添加部门条件
        if (deptName != null) {
            Predicate deptCondition = cb.equal(root.get("department"), deptName);
            conditions = cb.and(conditions, deptCondition);
        }

        // 如果最低薪资不为空,则添加薪资条件
        if (minSalary != null) {
            Predicate salaryCondition = cb.greaterThan(root.get("salary"), minSalary);
            conditions = cb.and(conditions, salaryCondition);
        }

        // 应用动态构建的条件
        query.where(conditions);

        List results = em.createQuery(query).getResultList();
        results.forEach(System.out::println);
    }

    /**
     * 案例 3: 使用表达式进行分组聚合
     * 我们不返回对象,而是返回 Double 类型的平均值。
     */
    public static void calculateAvgSalaryByDept(EntityManager em, String deptName) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        // 这里的泛型是 Double,因为我们想要返回一个数字
        CriteriaQuery query = cb.createQuery(Double.class);
        Root root = query.from(Employee.class);

        // 构建条件: 部门 = ?
        query.where(cb.equal(root.get("department"), deptName));

        // 构建 SELECT avg(salary)
        Expression avgSalary = cb.avg(root.get("salary"));
        query.select(avgSalary);

        Double result = em.createQuery(query).getSingleResult();
        System.out.println(deptName + " 部门的平均薪资为: " + (result != null ? result : 0.0));
    }

    /**
     * 辅助方法:初始化一些测试数据
     */
    private static void initSampleData(EntityManager em) {
        em.getTransaction().begin();
        try {
            // 简单检查是否已有数据
            Long count = em.createQuery("SELECT COUNT(e) FROM Employee e", Long.class).getSingleResult();
            if (count == 0) {
                em.persist(new Employee("张三", "IT", 80000));
                em.persist(new Employee("李四", "Sales", 40000));
                em.persist(new Employee("王五", "IT", 55000));
                em.persist(new Employee("赵六", "HR", 45000));
                em.persist(new Employee("Alice", "IT", 70000));
                System.out.println("测试数据已插入。");
            }
            em.getTransaction().commit();
        } catch (Exception e) {
            em.getTransaction().rollback();
            e.printStackTrace();
        }
    }
}

深入解析与最佳实践

上面的代码展示了基本用法,但在实际生产环境中,我们还需要注意一些细节。

#### 1. 元模型 (Metamodel) 的使用

在上面的例子中,我们使用字符串 INLINECODEdae0f578 来引用字段。虽然 Criteria API 比原生 SQL 好一点,但字符串依然存在写错的风险。更高级的用法是使用 JPA 元模型。Hibernate 可以自动生成 INLINECODEda868d6a 类,这样你就可以用 INLINECODEd0a0b5fe 来代替 INLINECODE7f3924b5,实现真正的 100% 类型安全。虽然没有字符串看起来“整洁”,但它是企业级项目的首选。

#### 2. 避免笛卡尔积 (Joins 的正确姿势)

如果你有关联关系(例如 Employee 有一个 Department 对象),使用 Criteria API 进行连接时非常直观:

// 假设 Employee 类里有一个 Department dept 对象
Join join = root.join("dept", JoinType.INNER);
query.where(cb.equal(join.get("name"), "Research"));

请记住,在使用 join 时,一定要明确 JoinType,避免不小心产生笛卡尔积,导致性能急剧下降。

#### 3. 性能优化建议

  • 分页: 永远不要在生产环境直接把 INLINECODEb04e60fe 用于可能返回百万行数据的表。使用 INLINECODE1f86175f 和 setFirstResult(0) 来实现分页。
  • 投影: 如果你只需要员工的姓名,而不是整个对象,不要查询整个 INLINECODEddd30dc4。使用 INLINECODEcd4a1eb6 或者构造函数表达式只获取你需要的数据,这将显著减少网络传输和内存消耗。

总结与进阶

在这篇文章中,我们通过实战案例学习了 JPA Criteria API 的核心组件和使用方法。我们从简单的条件查询开始,逐步构建了动态的多条件查询,甚至接触了聚合函数。

关键要点回顾:

  • CriteriaBuilder 是工厂,CriteriaQuery 是蓝图,Root 是起点。
  • 利用 Predicate 组合可以轻松应对复杂的动态查询需求,避免繁琐的字符串拼接。
  • 动态性类型安全是 Criteria API 相比 JPQL 的两大杀手级优势。

你的下一步行动:

在你自己的项目中尝试重构一段旧的查询代码。试着把那些复杂的、充满 if (param != null) sql += "..." 的代码改写为 Criteria API。你会发现代码不仅更易读,而且更难被破坏。一旦你习惯了这种面向对象的查询构建方式,你就很难再回得去了。祝你编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/23688.html
点赞
0.00 平均评分 (0% 分数) - 0