ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

TypeORM 关系加载策略全解:Eager 即时加载与 Lazy 懒加载(Promise 模式) TypeORM 关系加载策略全解Eager 即时加载与 Lazy 懒加载Promise 模式【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm本篇围绕 TypeORM 官方文档 5-eager-and-lazy-relations.md 展开系统讲解关系实体加载的两大内置策略**Eager relations即时/急切加载**与Lazy relations懒加载。你将掌握如何用eager: true让find*查询自动携带关联数据、如何通过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多对多场景为例双向关系中只需要在希望自动加载的一侧开启 eagerimport { 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()对find、findOne、findBy、findAndCount等find*系列方法而言Eager 关系都会被自动装配进查询。2.2 三条硬性约束Eager relations 并非万能开关使用上受以下规则限制只对find*方法生效repository.find()、manager.find()等find*系列会自动加载 eager 关系而一旦你使用QueryBuildereager relations 默认被禁用必须显式调用leftJoinAndSelect或innerJoinAndSelect才能加载该关系。其原因是QueryBuilder给予了开发者对 SQL 的完全控制权TypeORM 不再替你注入隐式 join。这一判断在 src/query-builder/SelectQueryBuilder.ts 等处的逻辑中得到体现只有主查询流程且满足条件时才应用 eager join。只允许在关系的一侧声明对同一条关系在两侧同时使用eager: true是被禁止的。否则会导致递归加载例如 A eager B、B eager A 时无限嵌套TypeORM 会在元数据构建阶段对这种情况进行拦截。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.ts 的getRelationJoinType静态方法中有精确实现其决策逻辑为若父级 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 Strategyjoin与query如果实体层级很深、嵌套 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 任何关系第二种写法结合显式relations与loadEagerRelations: false可以实现只取我点名的那一层其余一律不自动展开的精细化控制非常适合需要控制载荷体积的场景。实现层面src/find-options/FindOptionsUtils.ts 会在选项类型判定时识别该字段在查询装配流程中loadEagerRelations false会直接短路 eager join 逻辑相关判断见 src/query-builder/SelectQueryBuilder.ts即跳过自动装配、仅保留relations中显式声明的部分。loadEagerRelations选项同样存在于 src/find-options/FindOneOptions.ts 的选项类型定义中find、findOne、findAndCount等系列均可使用。三、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: PromiseQuestion[] }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: PromiseCategory[] }注意两侧属性的类型都是PromiseQuestion[]/PromiseCategory[]而非裸数组——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/OneToOneawait 后得到的是单个实体对多值关系OneToMany/ManyToManyawait 后得到的是数组。3.3 底层机制属性重定义与 Promise 状态缓存懒加载之所以能访问即触发查询是因为 TypeORM 在返回实体前对关系属性做了重定义。相关实现位于 src/query-builder/RelationLoader.ts 的enableLazyLoad方法它在实体上预留了三个内部标记位数据缓存位__{propertyName}__、加载 Promise 位__promise_{propertyName}__、已加载标志位__has_{propertyName}__用于区分数据为空与尚未加载两种状态因为空数组/null也是合法结果通过Object.defineProperty重新定义该关系属性getter若数据已加载直接返回Promise.resolve(已缓存数据)若加载进行中返回已存在的 Promise避免重复发查询否则立刻调用RelationLoader.load(...)发起一次独立的关系查询并把结果 Promise 存入 promise 位resolve 后写回数据缓存位并清除 Promise 位。对单值关系还会在结果为空数组时归一化为nullsetter接受 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 relationsLazy relations触发时机主实体被find*加载时自动触发首次访问属性await时触发声明方式关系装饰器选项{ eager: true }属性类型声明为Promise...生效范围仅find*系列QueryBuilder需手动leftJoinAndSelect访问任意 getter 均生效查询形态默认LEFT JOIN满足条件时自动升级INNER JOIN或配合relationLoadStrategy: query改走独立查询每次访问走独立查询结果在实体上缓存主要限制同一关系只允许一侧 eager嵌套过深会撑大单条 SQL实验性特性需要 async/await 心智模型加载控制relationLoadStrategy、loadEagerRelations、显式relations无额外开关访问即加载实用建议数据量不大、层级固定且几乎每次都需要关联数据的场景优先用 eager 并保持关系层级较浅当列表页或批量导出等场景一次性数据量巨大、或关系深度不可控时改用relationLoadStrategy: query或loadEagerRelations: false配合显式relations精确控制载荷懒加载则更适合主实体常读、关联数据低频偶发访问的业务路径但鉴于其实验性质建议先在核心链路外验证稳定性。而无论选择哪种模式基于QueryBuilder的查询始终是最可控的加载方式——它不会受到 eager 自动装配的隐式影响所有 join 都由你显式书写。五、继续深入仓库内相关资源官方 Relations 系列文档关系的基础概念与装饰器总览Eager and Lazy Relations 官方文档本文的原始依据Relations FAQ关系使用中的常见问题FindOptionsUtils.tsJOIN 类型推导与 eager join 递归装配实现SelectQueryBuilder.tsquery策略下独立查询加载 eager 关系的实现RelationLoader.ts懒加载 getter/setter 与 Promise 缓存的底层实现EntityMetadata.tseagerRelations关系元数据收集处。对本文涉及的任何结论均可通过阅读上述源码文件进一步求证也可在本仓库的test/目录下检索对应关系的功能测试用例观察实际 SQL 与行为表现。【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表