0

0

使用CriteriaQuery预加载嵌套关联集合的教程

花韻仙語

花韻仙語

发布时间:2025-09-21 10:46:33

|

774人浏览过

|

来源于php中文网

原创

使用CriteriaQuery预加载嵌套关联集合的教程

本文详细介绍了如何在Hibernate中使用CriteriaQuery预加载(eagerly fetch)子对象的嵌套关联集合。通过分析常见的预加载问题,特别是针对多层关联的集合类型,文章提供了一种基于链式fetch操作的解决方案,并辅以具体代码示例和原理分析,旨在帮助开发者高效地优化数据查询性能,避免N+1查询问题。

模型概述

在复杂的企业应用中,数据模型之间往往存在多层关联。考虑以下两个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();
CriteriaQuery criteriaQuery = 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实体内部的集合。

LobeHub
LobeHub

LobeChat brings you the best user experience of ChatGPT, OLLaMA, Gemini, Claude

下载

解决方案:链式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);
        }
    }
}

在上述代码中,关键在于以下两行:

  1. Fetch cargoFetch = root.fetch("cargo", JoinType.LEFT); 这一步从Funcionario的Root对象开始,预加载了cargo属性,并将返回的Fetch对象赋值给cargoFetch变量。Fetch表示这个Fetch操作是从Funcionario实体到Cargo实体。
  2. 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的连接,并将其标记为预加载。

注意事项与最佳实践

  1. 性能考量: 虽然预加载可以解决N+1问题,但过度或不恰当的预加载也可能导致性能问题。
    • 笛卡尔积: 如果在一个查询中预加载了多个ManyToMany或OneToMany集合,可能会导致数据库返回大量重复数据(笛卡尔积),增加网络传输和内存消耗。Hibernate通常会通过内部机制(如DISTINCT或分批加载)来缓解这个问题,但在某些情况下仍需注意。
    • 数据量: 预加载大量数据会增加查询时间和内存占用。应根据实际业务需求权衡。
  2. JoinType的选择:
    • JoinType.LEFT (左外连接): 会返回Root实体(Funcionario)的所有记录,即使其关联的Cargo或treinamentosNecessarios不存在。这是最常用的预加载方式,因为它不会过滤掉主实体。
    • JoinType.INNER (内连接): 只会返回Root实体(Funcionario)和所有被INNER JOIN的关联都存在的记录。如果Cargo或treinamentosNecessarios不存在,那么对应的Funcionario记录也不会被返回。
  3. FetchType.LAZY的重要性: 即使在实体定义中将关联设置为FetchType.LAZY,也可以通过CriteriaQuery的fetch方法强制进行预加载。这提供了灵活性,允许你在需要时进行预加载,而在不需要时保持延迟加载,从而更好地控制性能。
  4. 会话管理: 在示例代码中,Session的关闭逻辑被放置在finally块中。确保在所有数据库操作完成后,无论成功与否,都能正确关闭Session,以释放数据库连接资源。
  5. javax.persistence.criteria.Fetch与org.hibernate.query.criteria.internal.path.SingularAttributePath#fetch: 确保导入的是JPA标准的javax.persistence.criteria.Fetch。

总结

通过本文的讲解,我们理解了如何在Hibernate的CriteriaQuery中有效地预加载子对象的嵌套关联集合。核心方法是利用fetch方法返回的Fetch对象进行链式调用,从而实现多层关联的预加载。这种技术对于优化复杂数据模型的查询性能、避免N+1查询问题至关重要。在实际开发中,应结合业务需求和性能考量,合理运用预加载策略。

相关专题

更多
hibernate和mybatis有哪些区别
hibernate和mybatis有哪些区别

hibernate和mybatis的区别:1、实现方式;2、性能;3、对象管理的对比;4、缓存机制。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

140

2024.02.23

Hibernate框架介绍
Hibernate框架介绍

本专题整合了hibernate框架相关内容,阅读专题下面的文章了解更多详细内容。

82

2025.08.06

Java Hibernate框架
Java Hibernate框架

本专题聚焦 Java 主流 ORM 框架 Hibernate 的学习与应用,系统讲解对象关系映射、实体类与表映射、HQL 查询、事务管理、缓存机制与性能优化。通过电商平台、企业管理系统和博客项目等实战案例,帮助学员掌握 Hibernate 在持久层开发中的核心技能。

35

2025.09.02

Hibernate框架搭建
Hibernate框架搭建

本专题整合了Hibernate框架用法,阅读专题下面的文章了解更多详细内容。

64

2025.10.14

session失效的原因
session失效的原因

session失效的原因有会话超时、会话数量限制、会话完整性检查、服务器重启、浏览器或设备问题等等。详细介绍:1、会话超时:服务器为Session设置了一个默认的超时时间,当用户在一段时间内没有与服务器交互时,Session将自动失效;2、会话数量限制:服务器为每个用户的Session数量设置了一个限制,当用户创建的Session数量超过这个限制时,最新的会覆盖最早的等等。

314

2023.10.17

session失效解决方法
session失效解决方法

session失效通常是由于 session 的生存时间过期或者服务器关闭导致的。其解决办法:1、延长session的生存时间;2、使用持久化存储;3、使用cookie;4、异步更新session;5、使用会话管理中间件。

741

2023.10.18

cookie与session的区别
cookie与session的区别

本专题整合了cookie与session的区别和使用方法等相关内容,阅读专题下面的文章了解更详细的内容。

88

2025.08.19

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1027

2023.10.19

Java编译相关教程合集
Java编译相关教程合集

本专题整合了Java编译相关教程,阅读专题下面的文章了解更多详细内容。

0

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.7万人学习

C# 教程
C# 教程

共94课时 | 7.2万人学习

Java 教程
Java 教程

共578课时 | 48.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号