ORM 的 Eager Loading 与 Lazy Loading
区分 ORM 关联数据的 eager 与 lazy 获取时机,并说明它们与 API response shape、conditional include、batch loading 和 N+1 的关系。
#synthesis
#growing
#backend
#database
[!info] related notes
- 所属 MOC: 数据库 MOC
- 前置概念: 对象关系映射 ORM、SQL JOIN
- 并列概念: N+1 Query Problem
- 相关概念: GraphQL N+1 Query Problem
ORM 的 Eager Loading 与 Lazy Loading
范围
这篇讨论 ORM 获取“关联数据”的时机,不讨论前端代码分包意义上的 Lazy Loading。
核心区别
Eager Loading
→ 查询主体时已经知道需要关联数据,一起预取
Lazy Loading
→ 先取主体,真正访问关联时再发查询
例如:
User + Assessments 一开始就需要
→ eager / preload / include
先查 User,某个分支后来才需要 Teams
→ lazy getTeams()
API Response Shape 不是同一个维度
GET /users/1?include=favorites
这是 API 层决定“这次响应要什么”。后端看到参数后,可以立刻构造带关联的查询,这仍然属于 conditional eager loading。
因此:
API 是否返回关联数据
≠
ORM 是 eager 还是 lazy
Eager 的适用场景
- API contract 明确总是需要该关联;
- 批量列表中需要同一种关联数据;
- 希望避免逐对象访问导致 N+1。
Eager 不一定意味着“必须一条巨大 JOIN”。ORM 也可能用少量批量查询实现预取。
Lazy 的适用场景
- 关联只在少数业务分支中需要;
- 是否需要关联要根据刚查出的主体状态才能决定;
- 关联数据昂贵,而且绝大多数请求不会访问。
最大风险:N+1
const users = await findUsers()
for (const user of users) {
await user.getAssessments()
}
可能变成:
1 次 users 查询
+ N 次 assessments 查询
Lazy 本身不是错误;问题是把逐对象 lazy access 用在批量结果上,却没有预取或批处理。
实用决策规则
- 先由 API / use case 决定这次需要什么数据;
- 再由数据访问层选择 JOIN、preload、batch query 或 lazy query;
- 不要把“返回字段可配置”误认为“应该默认 lazy”;
- 看 ORM 代码时始终追问:最终会发几次 SQL?