在复杂的企业应用中,数据模型之间往往存在多层关联。考虑以下两个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实体内部的集合。
解决此问题的关键在于理解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操作的原理是,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的连接,并将其标记为预加载。
通过本文的讲解,我们理解了如何在Hibernate的CriteriaQuery中有效地预加载子对象的嵌套关联集合。核心方法是利用fetch方法返回的Fetch对象进行链式调用,从而实现多层关联的预加载。这种技术对于优化复杂数据模型的查询性能、避免N+1查询问题至关重要。在实际开发中,应结合业务需求和性能考量,合理运用预加载策略。