在当今的企业级 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。你会发现代码不仅更易读,而且更难被破坏。一旦你习惯了这种面向对象的查询构建方式,你就很难再回得去了。祝你编码愉快!