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;
✅ 优化效果:当需要加载多个 Customer
的 orders
时,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
这是一种介于 SELECT
和 JOIN
之间的折中方案,仅适用于集合类型的关联(如 @OneToMany
, @ManyToMany
)。
配置:
@OneToMany
@Fetch(FetchMode.SUBSELECT)
private Set<Order> orders;
✅ 核心行为:
- 第一次查询:获取所有匹配的
Customer
。 - 第二次查询:使用一个子查询,通过
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 特有):定义加载方式——用SELECT
、JOIN
还是SUBSELECT
来实现加载。
它们的协同规则如下:
FetchMode 设置 | FetchType 的行为 |
---|---|
未设置(默认) | 由 FetchType 决定 |
SELECT 或 SUBSELECT |
尊重 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 代码的关键。