
本文详细介绍了在jpa和hibernate环境中,如何根据关联表(外键关联实体)的属性值来筛选主实体数据。我们将探讨三种主要的实现方式:简洁直观的jpa jpql、类型安全且灵活的jpa criteria api,以及针对hibernate用户的传统criteria api。通过具体代码示例,本教程旨在帮助开发者理解并掌握在复杂数据关联场景下构建精确查询的技术。
在企业级应用开发中,数据模型通常包含多个相互关联的实体。当需要根据一个实体(主实体)的关联实体(外键关联)的特定属性值来检索主实体数据时,就需要构建跨越这些关联的查询。例如,从一个队列(Queue)实体中,筛选出属于特定地点(Location)和特定队列房间(QueueRoom)的所有队列。本教程将以一个Queue实体为例,演示如何高效地实现这类复合筛选。
1. 数据模型概览
假设我们有一个Queue实体,它与Location和QueueRoom实体存在多对一的关联关系:
import javax.persistence.*;
@Entity
@Table(name = "queue")
public class Queue {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "queue_id")
private Integer queueId;
@ManyToOne
@JoinColumn(name = "location_id", nullable = false)
private Location location; // 关联Location实体
@ManyToOne
@JoinColumn(name = "queue_room_id", nullable = true)
public QueueRoom queueRoom; // 关联QueueRoom实体
// Getters and Setters...
public Integer getQueueId() { return queueId; }
public void setQueueId(Integer queueId) { this.queueId = queueId; }
public Location getLocation() { return location; }
public void setLocation(Location location) { this.location = location; }
public QueueRoom getQueueRoom() { return queueRoom; }
public void setQueueRoom(QueueRoom queueRoom) { this.queueRoom = queueRoom; }
}
// Location 和 QueueRoom 实体结构类似,包含一个 String uuid 字段
@Entity
@Table(name = "location")
class Location {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String uuid;
// Getters and Setters...
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
}
@Entity
@Table(name = "queue_room")
class QueueRoom {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String uuid;
// Getters and Setters...
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
}我们的目标是根据location的uuid和queueRoom的uuid来查询Queue列表。
2. 使用 JPA JPQL (Java Persistence Query Language)
JPQL 是一种面向对象的查询语言,它在实体和它们的属性上操作,而不是直接在数据库表和列上操作。对于关联实体的属性筛选,JPQL 提供了一种非常直观和简洁的方式。
2.1 JPQL 查询语法
要实现基于关联表值的筛选,可以直接通过点号 (.) 访问关联实体的属性。多个条件可以使用 AND 逻辑运算符连接。
SELECT q FROM Queue q WHERE q.location.uuid = :locationUuid AND q.queueRoom.uuid = :queueRoomUuid
在上述JPQL查询中:
- SELECT q FROM Queue q:表示从Queue实体中选择所有Queue对象,并为其指定别名q。
- q.location.uuid:通过q访问其关联的location实体,再访问location实体的uuid属性。
- q.queueRoom.uuid:同理,访问queueRoom实体的uuid属性。
- :locationUuid 和 :queueRoomUuid:是命名参数,用于在执行查询时传入具体的值。
2.2 在 Java 代码中执行 JPQL 查询
通常通过 EntityManager 来创建和执行 JPQL 查询。
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
public class QueueRepository {
private EntityManager entityManager; // 注入或获取EntityManager实例
public QueueRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
public List getAllQueuesByLocationAndQueueRoom(String locationUuid, String queueRoomUuid) {
String jpql = "SELECT q FROM Queue q " +
"WHERE q.location.uuid = :locationUuid " +
"AND q.queueRoom.uuid = :queueRoomUuid";
TypedQuery query = entityManager.createQuery(jpql, Queue.class);
query.setParameter("locationUuid", locationUuid);
query.setParameter("queueRoomUuid", queueRoomUuid);
return query.getResultList();
}
} 优点:
- 简洁易读: 查询语句与SQL类似,但操作的是实体对象,更符合面向对象思维。
- 开发效率高: 对于固定查询,编写速度快。
缺点:
- 字符串拼接: 如果查询条件是动态的,可能需要进行字符串拼接,容易出错且不易维护。
- 缺乏编译时检查: JPQL语法错误只能在运行时发现。
3. 使用 JPA Criteria API
JPA Criteria API 是一种类型安全的、程序化的查询构建方式。它允许开发者通过 Java 代码构建查询,从而在编译时捕获潜在的错误,并方便地构建动态查询。
3.1 Criteria API 查询构建
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Predicate;
import java.util.List;
public class QueueRepository {
private EntityManager entityManager;
public QueueRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
public List getAllQueuesByLocationAndQueueRoomCriteria(String locationUuid, String queueRoomUuid) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery criteria = builder.createQuery(Queue.class);
Root queueRoot = criteria.from(Queue.class); // 定义查询的根实体
// 构建第一个条件:q.location.uuid = :locationUuid
Predicate locationPredicate = builder.equal(
queueRoot.get("location").get("uuid"), // 导航到 location 实体,再获取 uuid 属性
locationUuid
);
// 构建第二个条件:q.queueRoom.uuid = :queueRoomUuid
Predicate queueRoomPredicate = builder.equal(
queueRoot.get("queueRoom").get("uuid"), // 导航到 queueRoom 实体,再获取 uuid 属性
queueRoomUuid
);
// 使用 builder.and() 将两个条件组合起来
criteria.where(builder.and(locationPredicate, queueRoomPredicate));
return entityManager.createQuery(criteria).getResultList();
}
} 代码解析:
- CriteriaBuilder builder = entityManager.getCriteriaBuilder();:获取 CriteriaBuilder 实例,它是构建查询条件的工厂。
- CriteriaQuery
criteria = builder.createQuery(Queue.class);:创建一个 CriteriaQuery 对象,指定查询结果类型为 Queue。 - Root
queueRoot = criteria.from(Queue.class);:定义查询的根实体为 Queue,并为其创建 Root 对象。Root 对象代表查询的FROM子句。 - queueRoot.get("location").get("uuid"):通过 Root 对象的 get() 方法,可以安全地导航到关联实体及其属性。
- builder.equal(...):创建相等条件。
- builder.and(locationPredicate, queueRoomPredicate):使用 CriteriaBuilder 的 and() 方法将多个 Predicate (条件)组合起来,形成一个复合条件。
- criteria.where(...):将组合后的条件应用于查询。
- entityManager.createQuery(criteria).getResultList();:执行构建好的 Criteria 查询并获取结果列表。
优点:
- 类型安全: 在编译时检查属性名和类型,减少运行时错误。
- 动态查询: 易于根据不同条件动态地添加或移除查询谓词。
- 可读性高: 对于复杂的动态查询,代码结构比拼接JPQL字符串更清晰。
缺点:
- 冗长: 对于简单查询,代码量可能比JPQL多。
- 学习曲线: 需要熟悉Criteria API的各种接口和方法。
4. 使用 Hibernate Criteria API (Legacy)
虽然 JPA Criteria API 是推荐的标准化方式,但对于仍在使用旧版 Hibernate 或熟悉其原生 Criteria API 的开发者,了解其实现方式也很有价值。需要注意的是,Hibernate 原生 Criteria API 在 Hibernate 5.2 之后已被标记为废弃(Deprecated),并建议使用 JPA Criteria API。
4.1 Hibernate Criteria API 查询构建
原始问题中尝试的方法非常接近,但关键在于如何正确地将条件应用到根查询,并正确地执行查询。
import org.hibernate.Session;
import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType; // 用于指定连接类型
import java.util.List;
public class QueueRepository {
private Session currentSession; // 注入或获取Hibernate Session实例
public QueueRepository(Session currentSession) {
this.currentSession = currentSession;
}
@SuppressWarnings("unchecked")
public List getAllQueuesByLocationAndQueueRoomHibernateCriteria(String locationUuid, String queueRoomUuid) {
Criteria criteria = currentSession.createCriteria(Queue.class, "q");
// 创建别名并添加限制到根Criteria
// 使用 createAlias 而不是 createCriteria 来避免创建子 Criteria 并确保所有限制都在根级别
criteria.createAlias("q.location", "ql", JoinType.INNER_JOIN);
criteria.createAlias("q.queueRoom", "qr", JoinType.INNER_JOIN);
// 添加条件到根Criteria
criteria.add(Restrictions.eq("ql.uuid", locationUuid));
criteria.add(Restrictions.eq("qr.uuid", queueRoomUuid));
// 假设有一个 includeVoidedObjects 方法,这里也加入
// includeVoidedObjects(criteria, false); // 根据实际情况决定是否需要
return criteria.list();
}
} 代码解析:
- currentSession.createCriteria(Queue.class, "q"):创建针对 Queue 实体的根 Criteria 对象,并指定别名 q。
- criteria.createAlias("q.location", "ql", JoinType.INNER_JOIN);:通过 createAlias 方法为关联实体 location 创建一个别名 ql。这会在SQL查询中生成一个内部连接(INNER JOIN)。
- criteria.createAlias("q.queueRoom", "qr", JoinType.INNER_JOIN);:同理为 queueRoom 创建别名 qr。
- criteria.add(Restrictions.eq("ql.uuid", locationUuid));:将针对 location 实体 uuid 的条件添加到根 criteria 对象。
- criteria.add(Restrictions.eq("qr.uuid", queueRoomUuid));:将针对 queueRoom 实体 uuid 的条件添加到根 criteria 对象。
- criteria.list():执行查询并返回结果列表。
注意事项:
- createAlias vs createCriteria: createAlias 用于创建关联的别名并将限制添加到父 Criteria,而 createCriteria 会返回一个新的 Criteria 实例,其限制默认只作用于该子 Criteria。在需要组合多个关联实体的条件时,通常使用 createAlias 更为直接。
- 废弃状态: 再次强调,Hibernate 原生 Criteria API 已被废弃,新项目应优先使用 JPA Criteria API。
5. 选择合适的查询方式
- JPQL: 适用于固定且相对简单的查询,或者当您更倾向于使用类似SQL的字符串查询时。它的可读性很好。
- JPA Criteria API: 推荐用于需要构建动态查询、追求类型安全和在编译时捕获错误的场景。虽然代码可能更详细,但它提供了更大的灵活性和健壮性。
- Hibernate Criteria API (Legacy): 仅当您在维护使用旧版 Hibernate 的遗留系统时才考虑使用。对于新开发,请避免使用。
6. 总结与最佳实践
在JPA/Hibernate中,根据关联表值进行筛选是常见的需求。无论是通过JPQL的简洁语法,还是JPA Criteria API的类型安全和动态构建能力,都能够有效地实现这一目标。
最佳实践:
- 索引: 确保关联表的外键列和用于筛选的属性(如uuid)都建立了索引,以优化查询性能。
- N+1问题: 如果查询结果需要访问关联实体的其他属性,考虑使用FETCH JOIN(JPQL)或join的FetchType(Criteria API)来避免N+1查询问题,一次性加载所有所需数据。
- 事务管理: 确保所有数据库操作都在合适的事务中执行。
- 错误处理: 对查询可能抛出的异常进行适当的处理。
通过掌握这些查询技术,开发者可以更灵活、高效地处理复杂的数据检索场景,构建健壮且高性能的持久层。










