首页
/ TypeORM 关系加载策略全解:Eager 即时加载与 Lazy 懒加载(Promise 模式)

TypeORM 关系加载策略全解:Eager 即时加载与 Lazy 懒加载(Promise 模式)

2026-09-08 14:13:31作者:柏廷章Berta

本篇围绕 TypeORM 官方文档 5-eager-and-lazy-relations.md 展开,系统讲解关系实体加载的两大内置策略:**Eager relations(即时/急切加载)**与 Lazy relations(懒加载)。你将掌握如何用 eager: truefind* 查询自动携带关联数据、如何通过 relationLoadStrategy 在 JOIN 与独立查询之间取舍、如何用 loadEagerRelations 精确控制加载行为,以及基于 Promise 类型的懒加载关系在保存与读取时的正确用法,并深入源码理解其底层 JOIN 推导与 Promise 封装原理。文中代码与结论均可在当前 TypeORM 仓库中直接验证与复现。

一、两种关系加载模式概览

在 TypeORM 中,实体之间的关系(@OneToOne@ManyToOne@OneToMany@ManyToMany)默认是"惰性"的——仅当你显式 join 或配置加载选项时才会取回数据。为减少手工编写关系加载代码,TypeORM 提供了两种内建模式:

  • Eager relations(即时加载):每次从数据库加载实体时,关联数据被自动一起取出,无需显式指定。
  • Lazy relations(懒加载):关联数据在你访问该属性时才被加载,属性类型必须声明为 Promise

两种模式都建立在同一个核心类之上:实体的元数据 EntityMetadata 中维护了 eagerRelations: RelationMetadata[] 数组(见 src/metadata/EntityMetadata.ts),凡是声明为 eager 的关系都会被收集进该列表,供后续查询装配逻辑读取。

二、Eager relations:让关联随主实体自动加载

2.1 声明方式与核心示例

在关系装饰器的选项对象中传入 eager: true 即可声明为 eager。以文档中的 Question/Category 多对多场景为例,双向关系中只需要在希望自动加载的一侧开启 eager:

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"
import { Question } from "./Question"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @ManyToMany((type) => Question, (question) => question.categories)
    questions: Question[]
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany((type) => Category, (category) => category.questions, {
        eager: true,
    })
    @JoinTable()
    categories: Category[]
}

这里的要点是:Question 侧通过选项 { eager: true } 声明了其对 Category 的关系为 eager,而 Category 侧保持普通关系。查询时不需要再 join 或声明要加载哪些关系:

const questionRepository = dataSource.getRepository(Question)

// questions 会连同其 categories 一起被加载
const questions = await questionRepository.find()

findfindOnefindByfindAndCountfind* 系列方法而言,Eager 关系都会被自动装配进查询。

2.2 三条硬性约束

Eager relations 并非万能开关,使用上受以下规则限制:

  1. 只对 find* 方法生效repository.find()manager.find()find* 系列会自动加载 eager 关系;而一旦你使用 QueryBuilder,eager relations 默认被禁用,必须显式调用 leftJoinAndSelect(或 innerJoinAndSelect)才能加载该关系。其原因是 QueryBuilder 给予了开发者对 SQL 的完全控制权,TypeORM 不再替你注入隐式 join。这一判断在 src/query-builder/SelectQueryBuilder.ts 等处的逻辑中得到体现:只有主查询流程且满足条件时才应用 eager join。

  2. 只允许在关系的一侧声明:对同一条关系,在两侧同时使用 eager: true被禁止的。否则会导致递归加载(例如 A eager B、B eager A 时无限嵌套),TypeORM 会在元数据构建阶段对这种情况进行拦截。

  3. eager 与 relations 显式声明可以并存:即使关系被标记为 eager,你在 find 选项里仍然可以显式列出它,二者不会冲突(详见下文 loadEagerRelations 一节)。

2.3 默认 LEFT JOIN 与自动升级的 INNER JOIN

官方文档明确指出:默认情况下,eager relations 使用 LEFT JOIN 加载;而如果该关系同时满足两个条件——nullable: false拥有连接列(即关系的拥有方,典型如 @ManyToOne 或拥有方的 @OneToOne)——TypeORM 会改用 INNER JOIN,从而可能产生更高效的查询计划。

这一行为在 src/find-options/FindOptionsUtils.tsgetRelationJoinType 静态方法中有精确实现,其决策逻辑为:

  • 若父级 join 类型为 LEFT,则所有后代关系必须继承 LEFT——否则会把父别名(parent alias)为 NULL 的行过滤掉,导致数据丢失;
  • 当关系满足 !relation.isNullable && relation.isWithJoinColumn(不可空且拥有 join 列)时,进一步检查目标实体是否含软删除列(deleteDateColumn):
    • 若目标实体没有软删除列,或当前查询开启了 withDeleted,则返回 "inner"(使用 INNER JOIN);
    • 否则仍然返回 "left"

随后 joinEagerRelations 递归遍历 metadata.eagerRelations,为每个 eager 关系生成基于驱动别名规则的关系别名(如 Question__categories),判断是否已存在同类 join/select(避免重复 join),再依据 getRelationJoinType 的结果调用 innerJoin/leftJoin 并把别名加入 addSelect,最后递归处理嵌套实体的 eager 关系,形成可多层的自动关联树。文档中"它们会被自动加载"的承诺,正是由这条递归装配链实现的。

2.4 关系加载策略(Relation Load Strategy):joinquery

如果实体层级很深、嵌套 join 过多,单条 SQL 会膨胀为笛卡尔积式的大结果集(尤其 OneToMany/ManyToMany 场景会产生行倍增),带来网络与解析开销。为此 TypeORM 引入 relationLoadStrategy 选项,提供两种策略:

策略 行为 适用场景
"join"(默认) eager/指定关系通过向主查询追加 SQL JOIN 一次性取出 关系层级较浅、数据量可控
"query" 关联数据通过额外的独立数据库查询分别加载 嵌套 join 过深、一次性数据量过大

两种方式均可在单次查询DataSource 全局默认两个粒度配置:

// 逐查询指定
const questions = await questionRepository.find({
    relationLoadStrategy: "query",
})

// 或将 "query" 设为整个 DataSource 的默认策略
const dataSource = new DataSource({
    // ... 其余连接配置
    relationLoadStrategy: "query",
})

从源码看,relationLoadStrategy 会被写入查询表达式状态 expressionMap.relationLoadStrategy(见 src/query-builder/SelectQueryBuilder.ts),并在两处分叉:

  • 在选项解析阶段,当策略为 "query" 时,关系不再通过 leftJoinAndSelect 立即装配,而是调用 concatRelationMetadata(...) 把关系元数据收集起来(见 src/find-options/FindOptionsUtils.ts),留待主查询结束后统一以附加查询加载;
  • 在主查询完成后,若策略为 "query",会创建 QueryStrategyRelationIdLoader 并以递归方式对每个 eager 关系发起独立查询(见 src/query-builder/SelectQueryBuilder.ts)。源码中还能看到其对循环 eager 链(如 A→B→C→A)的防无限递归处理——每个分支维护独立的"已访问"集合,避免并行分支相互干扰。

因此,面对深嵌套关系导致的主查询膨胀,切换到 "query" 策略往往能让单条 SQL 回归精简,代价是增加数据库往返次数,需要在两者间按实际数据量与网络往返成本权衡。

2.5 用 loadEagerRelations 精确开关 Eager 加载

find* 选项还提供 loadEagerRelations 布尔开关,用于对 eager 行为做更细的裁剪:

// 完全禁用 eager 关系加载
const questions = await questionRepository.find({
    loadEagerRelations: false,
})

// 只加载显式声明的 relations,抑制实体上其余嵌套 eager 关系
const questions = await questionRepository.find({
    relations: { categories: true },
    loadEagerRelations: false,
})

第一种写法下,即使实体属性声明了 eager: true,本次查询也不会自动 join 任何关系;第二种写法结合显式 relationsloadEagerRelations: false,可以实现"只取我点名的那一层,其余一律不自动展开"的精细化控制,非常适合需要控制载荷体积的场景。

实现层面,src/find-options/FindOptionsUtils.ts 会在选项类型判定时识别该字段;在查询装配流程中 loadEagerRelations === false 会直接短路 eager join 逻辑(相关判断见 src/query-builder/SelectQueryBuilder.ts),即跳过自动装配、仅保留 relations 中显式声明的部分。loadEagerRelations 选项同样存在于 src/find-options/FindOneOptions.ts 的选项类型定义中,findfindOnefindAndCount 等系列均可使用。

三、Lazy relations:基于 Promise 的按需加载

3.1 概念与类型要求

与 eager 相反,lazy relations 中的实体在你访问属性时才被加载。一个关键约束是:这类关系的属性类型必须是 Promise——保存时你把值包装进一个 Promise,读取时拿到的也是一个 Promise。

以同一对 Question/Category 为例,若希望二者互为懒加载:

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"
import { Question } from "./Question"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @ManyToMany((type) => Question, (question) => question.categories)
    questions: Promise<Question[]>
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany((type) => Category, (category) => category.questions)
    @JoinTable()
    categories: Promise<Category[]>
}

注意两侧属性的类型都是 Promise<Question[]> / Promise<Category[]> 而非裸数组——categories 是一个 Promise,这正是"懒"的载体:类型系统时刻提醒你,该属性内部存放的是一个"未来才就绪的值"。

3.2 保存与读取

保存懒加载关系:需要将待关联实体用 Promise.resolve(...) 包装后赋给属性:

const category1 = new Category()
category1.name = "animals"
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = "zoo"
await dataSource.manager.save(category2)

const question = new Question()
question.categories = Promise.resolve([category1, category2])
await dataSource.manager.save(question)

读取懒加载关系:先取回主实体,再 await 该 Promise 属性:

const [question] = await dataSource.getRepository(Question).find()
const categories = await question.categories
// 此刻 question 的全部 categories 都已在 "categories" 变量中

对单值关系(@ManyToOne/@OneToOne),await 后得到的是单个实体;对多值关系(@OneToMany/@ManyToMany),await 后得到的是数组。

3.3 底层机制:属性重定义与 Promise 状态缓存

懒加载之所以能"访问即触发查询",是因为 TypeORM 在返回实体前对关系属性做了重定义。相关实现位于 src/query-builder/RelationLoader.tsenableLazyLoad 方法:

  • 它在实体上预留了三个内部标记位:数据缓存位 __{propertyName}__、加载 Promise 位 __promise_{propertyName}__、已加载标志位 __has_{propertyName}__(用于区分"数据为空"与"尚未加载"两种状态,因为空数组/null 也是合法结果);
  • 通过 Object.defineProperty 重新定义该关系属性:
    • getter:若数据已加载,直接返回 Promise.resolve(已缓存数据);若加载进行中,返回已存在的 Promise(避免重复发查询);否则立刻调用 RelationLoader.load(...) 发起一次独立的关系查询,并把结果 Promise 存入 promise 位,resolve 后写回数据缓存位并清除 Promise 位。对单值关系,还会在结果为空数组时归一化为 null
    • setter:接受 Promise 或直接值。若赋入的是 Promise,则在它 resolve 后再落盘(setPromise 内部带有"确保仍是当前 promise 才写入"的守卫,防止过期请求覆盖新值);若直接赋普通值则立即写入。

也就是说,每次 await question.categories 首次触发时,TypeORM 会依据 RelationMetadata 中该关系的外键/连接表信息,拼接一条按需执行的独立 SELECT 把关联数据取回,并在实体生命周期内缓存,后续访问不再重复查询。

3.4 适用注意事项

官方文档特别强调:如果你来自 Java、PHP 等同步模型语言并习惯"到处懒加载",需要格外小心——这些语言没有异步机制,其懒加载基于代理对象实现;而在 JavaScript / Node.js 中,Promise 是异步的必然载体,因此 TypeORM 的 lazy relations 必须依赖 Promise 实现,这属于非标准技术,在 TypeORM 中仍被视为**实验性(experimental)**特性。这意味着其 API 与行为可能在未来版本变化,生产环境大规模使用前建议评估风险,并将"与同步语言心智模型的差异"纳入团队约定。

四、Eager 与 Lazy 的选型建议与总结

维度 Eager relations Lazy relations
触发时机 主实体被 find* 加载时自动触发 首次访问属性(await)时触发
声明方式 关系装饰器选项 { eager: true } 属性类型声明为 Promise<...>
生效范围 find* 系列;QueryBuilder 需手动 leftJoinAndSelect 访问任意 getter 均生效
查询形态 默认 LEFT JOIN(满足条件时自动升级 INNER JOIN),或配合 relationLoadStrategy: "query" 改走独立查询 每次访问走独立查询,结果在实体上缓存
主要限制 同一关系只允许一侧 eager;嵌套过深会撑大单条 SQL 实验性特性;需要 async/await 心智模型
加载控制 relationLoadStrategyloadEagerRelations、显式 relations 无额外开关,访问即加载

实用建议:数据量不大、层级固定且几乎每次都需要关联数据的场景,优先用 eager 并保持关系层级较浅;当列表页或批量导出等场景一次性数据量巨大、或关系深度不可控时,改用 relationLoadStrategy: "query"loadEagerRelations: false 配合显式 relations 精确控制载荷;懒加载则更适合"主实体常读、关联数据低频偶发访问"的业务路径,但鉴于其实验性质,建议先在核心链路外验证稳定性。而无论选择哪种模式,基于 QueryBuilder 的查询始终是最可控的加载方式——它不会受到 eager 自动装配的隐式影响,所有 join 都由你显式书写。

五、继续深入:仓库内相关资源

对本文涉及的任何结论,均可通过阅读上述源码文件进一步求证,也可在本仓库的 test/ 目录下检索对应关系的功能测试用例观察实际 SQL 与行为表现。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393