1. 简介

本文将深入探讨 Hibernate 中 @org.hibernate.annotations.Fetch 注解所支持的几种 FetchMode 模式。这些模式直接影响关联数据的加载方式,对性能有显著影响。理解它们的区别,能帮你避免常见的“n+1 查询”踩坑。

2. 示例准备

我们以一个简单的客户-订单模型为例。Customer 实体包含 ID 和其名下的订单集合:

@Entity
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "customer")
    @Fetch(value = FetchMode.SELECT)
    private Set<Order> orders = new HashSet<>();

    // getters and setters
}

对应的 Order 实体如下:

@Entity
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // getters and setters
}

在接下来的每个场景中,我们都会执行以下代码来获取客户及其订单:

Customer customer = customerRepository.findById(id).get();
Set<Order> orders = customer.getOrders();

我们的关注点在于:这段代码会触发多少条 SQL 查询?查询是如何组织的?

3. FetchMode.SELECT

这是最直观但也最容易引发性能问题的模式。我们通过 @Fetch(FetchMode.SELECT) 明确指定使用 SELECT 模式。

核心行为:懒加载(Lazy Loading)。

当执行第一行:

Customer customer = customerRepository.findById(id).get();

Hibernate 只会查询 Customer 表:

Hibernate: 
    select ... from customer 
    where customer0_.id=?

此时 orders 集合并未加载,只是一个代理。

⚠️ 当执行第二行:

Set<Order> orders = customer.getOrders();

Hibernate 才会为这个 Customer 发起额外的查询去加载 orders

Hibernate: 
    select ... from order 
    where order0_.customer_id=?

3.1. n+1 查询问题

如果一次加载多个 Customer,比如:

List<Customer> customers = customerRepository.findAll();
for (Customer c : customers) {
    c.getOrders().size(); // 触发加载
}

结果将是:1 次查询加载所有客户 + 每个客户 1 次查询加载其订单。假设有 5 个客户,就会产生 6 次 SQL 查询(1 + 5),这就是臭名昭著的 n+1 查询问题

3.2. 使用 @BatchSize 优化

为了缓解 n+1 问题,可以结合 @BatchSize 注解:

@OneToMany
@Fetch(FetchMode.SELECT)
@BatchSize(size = 10)
private Set<Order> orders;

优化效果:当需要加载多个 Customerorders 时,Hibernate 会将它们的 ID 收集起来,用 IN 条件批量加载。

例如,加载 5 个客户时,orders 的查询会变成:

Hibernate: 
    select ... from order 
    where order0_.customer_id in (?, ?, ?, ?, ?)

这样,查询次数从 1 + 5 = 6 次,减少到 1 + 1 = 2 次,性能提升显著。

4. FetchMode.JOIN

SELECT 的懒加载不同,JOIN 模式是急加载(Eager Loading)。

配置方式:

@OneToMany
@Fetch(FetchMode.JOIN)
private Set<Order> orders;

核心行为:在查询 Customer 的同时,通过 LEFT OUTER JOIN 一次性把关联的 Order 数据也查出来。

执行 customerRepository.findById(id).get() 时,生成的 SQL 是:

Hibernate: 
    select ...
    from
        customer customer0_ 
    left outer join
        order order1_ 
            on customer0_.id=order1_.customer_id 
    where
        customer0_.id=?

优点:只用 1 次查询就完成了数据获取,彻底避免了 n+1 问题。

缺点

  • 数据重复:Customer 的信息会在结果集中重复 N 次(N = 订单数),网络和内存开销大。
  • 不支持分页:对集合使用 JOIN 后,LIMIT/OFFSET 会作用于连接后的结果集,导致分页不准确。

5. FetchMode.SUBSELECT

这是一种介于 SELECTJOIN 之间的折中方案,仅适用于集合类型的关联(如 @OneToMany, @ManyToMany)。

配置:

@OneToMany
@Fetch(FetchMode.SUBSELECT)
private Set<Order> orders;

核心行为

  1. 第一次查询:获取所有匹配的 Customer
  2. 第二次查询:使用一个子查询,通过 IN 条件加载所有相关 Order

具体 SQL 如下:

第一次查询 Customer

Hibernate: 
    select ... from customer customer0_ 
    where customer0_.id=?

第二次查询 Order

Hibernate: 
    select ...
    from
        order order0_ 
    where
        order0_.customer_id in (
            select
                customer0_.id 
            from
                customer customer0_
            where customer0_.id=?
        )

优点

  • 避免了 JOIN 的数据重复问题。
  • SELECT + 无 BatchSize 更高效,尤其是在批量加载时。

注意:子查询中的 SELECT 语句会复用主查询的条件,但可能不够灵活。

6. FetchMode 与 FetchType 的关系

这是最容易混淆的点。简单来说:

  • FetchType(JPA 标准):定义加载策略——LAZY(懒加载)还是 EAGER(急加载)。
  • FetchMode(Hibernate 特有):定义加载方式——用 SELECTJOIN 还是 SUBSELECT 来实现加载。

它们的协同规则如下:

FetchMode 设置 FetchType 的行为
未设置(默认) FetchType 决定
SELECTSUBSELECT 尊重 FetchType 的设置(LAZY 就懒,EAGER 就急)
JOIN 强制覆盖 FetchType,始终为急加载(Eager),无论 FetchType 设为何值

📌 关键结论:一旦使用了 @Fetch(FetchMode.JOIN)@OneToMany(fetch = FetchType.LAZY) 也会失效,数据会被急加载。

更多关于懒加载/急加载的细节,可参考 Hibernate 官方文档。

7. 总结

  • FetchMode.SELECT:懒加载,易导致 n+1 问题,可用 @BatchSize 优化。
  • FetchMode.JOIN:急加载,1 次查询搞定,但有数据重复和分页陷阱。
  • FetchMode.SUBSELECT:集合专用,批量加载时性能好,避免数据重复。
  • FetchMode 决定“怎么查”,FetchType 决定“何时查”,但 JOIN 模式会强制覆盖 FetchType

选择哪种模式,取决于你的具体场景:是单条记录查询、批量加载,还是需要分页?理解这些模式的底层 SQL 行为,是写出高效 Hibernate 代码的关键。


原始标题:FetchMode in Hibernate | Baeldung