
模型概述
在复杂的企业应用中,数据模型之间往往存在多层关联。考虑以下两个hibernate实体模型:funcionario(员工)和cargo(职位)。
Funcionario 模型
Funcionario实体代表员工信息,其中包含一个与Cargo实体建立的ManyToOne关联。
@Entity
@Table(name = "funcionarios")
public class Funcionario extends Model {
// ... 其他属性
@NotFound(action = NotFoundAction.IGNORE)
@ManyToOne(fetch = FetchType.LAZY, optional = true)
private Cargo cargo;
// ... 其他属性
}Cargo 模型
Cargo实体代表职位信息,其中包含一个与Treinamento(培训)实体建立的ManyToMany关联,表示该职位所需的培训列表。
@Entity
@Table(name = "cargos")
public class Cargo extends Model {
@Column(nullable = false, unique = true, columnDefinition = "TEXT")
private String cargo = "";
@ManyToMany(fetch = FetchType.LAZY)
private Set treinamentosNecessarios;
// ... 其他属性
} 上述模型中,Funcionario与Cargo是延迟加载(FetchType.LAZY),Cargo与treinamentosNecessarios集合也是延迟加载。这意味着在默认情况下,当我们查询Funcionario时,其关联的Cargo对象以及Cargo对象中的treinamentosNecessarios集合都不会被立即加载,只有在首次访问时才会触发额外的数据库查询。
问题描述与初始尝试
为了避免N+1查询问题,提高查询效率,我们通常希望在查询Funcionario时,能够同时预加载其关联的Cargo对象,以及Cargo对象内部的treinamentosNecessarios集合。
在使用Hibernate的CriteriaQuery进行预加载时,直接预加载Funcionario的Cargo属性相对简单:
CriteriaBuilder cb = session.getCriteriaBuilder(); CriteriaQuerycriteriaQuery = cb.createQuery(Funcionario.class); Root root = criteriaQuery.from(Funcionario.class); // 预加载 Cargo 属性 root.fetch("cargo", JoinType.LEFT); // ... 其他 fetch 或条件 criteriaQuery.select(root); // ... 执行查询
然而,如果尝试直接通过点号路径(如"cargo.treinamentosNecessarios")在Root对象上进行更深层次的集合预加载,例如:
// 这种方式通常无法直接在 Root 上工作,因为它不是直接关联的属性
// root.fetch("cargo.treinamentosNecessarios", JoinType.LEFT); 这种直接在Root上使用点号路径预加载嵌套集合的方式是无效的,因为root代表的是Funcionario实体,它不直接包含treinamentosNecessarios属性。treinamentosNecessarios是Cargo实体内部的集合。
解决方案:链式Fetch操作
解决此问题的关键在于理解CriteriaQuery中fetch方法的返回值。fetch方法返回一个Fetch对象,该对象代表了被预加载的关联。我们可以利用这个Fetch对象来继续预加载其内部的关联。
具体来说,当我们在Root上调用fetch("cargo", JoinType.LEFT)时,它会返回一个代表Cargo关联的Fetch对象。然后,我们可以在这个Fetch对象上再次调用fetch("treinamentosNecessarios", JoinType.LEFT),从而实现嵌套关联集合的预加载。
代码示例
以下是使用链式fetch操作预加载Funcionario的Cargo及其treinamentosNecessarios集合的完整代码示例:
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Fetch; // 注意这里导入的是 javax.persistence.criteria.Fetch
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Root;
import org.hibernate.query.Query;
import org.hibernate.Session; // 假设 session 已经获取
public class FuncionarioDao {
public Funcionario findWithEagerCargoAndTreinamentos(Long id, Session session) {
try {
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery criteriaQuery = cb.createQuery(Funcionario.class);
Root root = criteriaQuery.from(Funcionario.class);
// 1. 预加载 Funcionario 的 Cargo 属性
// fetch 方法返回一个 Fetch 对象,代表了预加载的 Cargo 关联
Fetch cargoFetch = root.fetch("cargo", JoinType.LEFT);
// 2. 在 Cargo 的 Fetch 对象上继续预加载 treinamentosNecessarios 集合
// 注意:这里是 cargoFetch.fetch(...),而不是 root.fetch(...)
cargoFetch.fetch("treinamentosNecessarios", JoinType.LEFT);
// 如果还有其他需要预加载的直接关联,可以继续在 root 上调用 fetch
// root.fetch("avaliacoes", JoinType.LEFT);
// root.fetch("treinamentosRealizados", JoinType.LEFT);
criteriaQuery.select(root);
criteriaQuery.where(cb.equal(root.get("id"), id));
Query query = session.createQuery(criteriaQuery);
Funcionario singleResult = query.getSingleResult();
return singleResult;
} catch (Exception ex) {
// 适当的异常处理
throw new RuntimeException("查询员工及其关联信息失败", ex);
}
}
} 在上述代码中,关键在于以下两行:
- Fetch
cargoFetch = root.fetch("cargo", JoinType.LEFT); 这一步从Funcionario的Root对象开始,预加载了cargo属性,并将返回的Fetch对象赋值给cargoFetch变量。Fetch 表示这个Fetch操作是从Funcionario实体到Cargo实体。 - cargoFetch.fetch("treinamentosNecessarios", JoinType.LEFT); 接着,我们利用上一步获取的cargoFetch对象,在其上继续调用fetch方法,预加载Cargo实体内部的treinamentosNecessarios集合。这样就成功实现了多层嵌套关联的预加载。
核心原理分析
这种链式fetch操作的原理是,javax.persistence.criteria.Fetch接口扩展了javax.persistence.criteria.Join接口,而Join接口又扩展了javax.persistence.criteria.Path接口。Path接口提供了fetch方法,允许我们从当前的路径继续向下预加载关联。
当root.fetch("cargo", JoinType.LEFT)执行时,它实际上构建了一个从Funcionario到Cargo的连接,并标记Cargo为预加载。返回的Fetch对象本质上代表了这个连接的“终点”——即Cargo实体。因此,我们可以在这个Cargo的“终点”上继续构建到treinamentosNecessarios的连接,并将其标记为预加载。
注意事项与最佳实践
-
性能考量: 虽然预加载可以解决N+1问题,但过度或不恰当的预加载也可能导致性能问题。
- 笛卡尔积: 如果在一个查询中预加载了多个ManyToMany或OneToMany集合,可能会导致数据库返回大量重复数据(笛卡尔积),增加网络传输和内存消耗。Hibernate通常会通过内部机制(如DISTINCT或分批加载)来缓解这个问题,但在某些情况下仍需注意。
- 数据量: 预加载大量数据会增加查询时间和内存占用。应根据实际业务需求权衡。
-
JoinType的选择:
- JoinType.LEFT (左外连接): 会返回Root实体(Funcionario)的所有记录,即使其关联的Cargo或treinamentosNecessarios不存在。这是最常用的预加载方式,因为它不会过滤掉主实体。
- JoinType.INNER (内连接): 只会返回Root实体(Funcionario)和所有被INNER JOIN的关联都存在的记录。如果Cargo或treinamentosNecessarios不存在,那么对应的Funcionario记录也不会被返回。
- FetchType.LAZY的重要性: 即使在实体定义中将关联设置为FetchType.LAZY,也可以通过CriteriaQuery的fetch方法强制进行预加载。这提供了灵活性,允许你在需要时进行预加载,而在不需要时保持延迟加载,从而更好地控制性能。
- 会话管理: 在示例代码中,Session的关闭逻辑被放置在finally块中。确保在所有数据库操作完成后,无论成功与否,都能正确关闭Session,以释放数据库连接资源。
- javax.persistence.criteria.Fetch与org.hibernate.query.criteria.internal.path.SingularAttributePath#fetch: 确保导入的是JPA标准的javax.persistence.criteria.Fetch。
总结
通过本文的讲解,我们理解了如何在Hibernate的CriteriaQuery中有效地预加载子对象的嵌套关联集合。核心方法是利用fetch方法返回的Fetch对象进行链式调用,从而实现多层关联的预加载。这种技术对于优化复杂数据模型的查询性能、避免N+1查询问题至关重要。在实际开发中,应结合业务需求和性能考量,合理运用预加载策略。










