资讯动态

TypeORM 关系查询构建器(RelationQueryBuilder)实战指南:高效读写实体关系

发布时间:2026/9/9 14:00:14 来源:尧图企业网站定制
TypeORM 关系查询构建器RelationQueryBuilder实战指南高效读写实体关系【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeormRelationQueryBuilder 是 TypeORM 中用于专门操作实体关系relations的查询构建器无需加载整个实体对象即可在数据库层面完成关系的绑定、解绑与赋值也能按需加载关联实体。本文围绕官方指南 5-relational-query-builder.md 展开并深入到 RelationQueryBuilder 源码、RelationUpdater、RelationRemover 与测试用例讲透 add / remove / set / loadMany / loadOne 等核心操作及其底层 SQL 行为帮助你写出最小开销、可上生产的关联操作代码。为什么需要专门的关系查询构建器常规修改一对多或多对多关系的做法是先查出目标实体并带上关联数据然后在内存数组里 push 或 splice最后调用save整体写回。以「给 id 为 1 的 Post 追加一个 Category」为例等价写法是const postRepository dataSource.manager.getRepository(Post) const post await postRepository.findOne({ where: { id: 1, }, relations: { categories: true, }, }) post.categories.push(category) await postRepository.save(post)这段代码存在两个明显问题操作数多、开销大一次 find含关联加载加上一次完整save会触发大量额外查询与变更检测数据规模不可控若某个 post 下已有上万条 category为了追加一条你必须先把这一万条全部 load 进内存再保存几乎无法在生产环境使用。RelationQueryBuilder正是为解决这类场景而存在它直接在数据库中执行最小的关系变更语句并且完全不需要先加载任何一方实体的完整数据。同样的需求用关系查询构建器只需一行await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) .add(category)代码要表达的语义是针对Post实体的categories这个多对多关系找到实体post把category追加bind进去。相比庞大的save调用它只执行极少的必要操作就在数据库层面完成了实体间的绑定RelationQueryBuilder.ts。关系查询构建器在整个查询构建器体系中的位置在 TypeORM 中RelationQueryBuilder继承自统一的抽象基类QueryBuilder同族还有我们熟悉的 Select / Insert / Update / Delete 各查询构建器。它的入口是基类上的relation()方法既支持传入实体目标 属性路径relation(Post, categories)也支持在已经关联了某个实体别名的构建器上直接传属性路径relation(categories)。从源码看QueryBuilder.relation() 内部会将expressionMap.queryType置为relation、记录relationPropertyPath并经由构建器注册表返回一个RelationQueryBuilder实例。构建器在链式调用后真正干活的是三类底层执行器RelationQueryBuilder.ts 分别委托给它们RelationUpdaterRelationUpdater.ts——支撑set与add两个操作RelationRemoverRelationRemover.ts——支撑remove操作RelationLoaderRelationLoader.ts——支撑loadOne/loadMany的关联加载。为关系追加实体addadd用于在many-to-many和one-to-many关系上新增绑定。第一个参数of()指定「谁的关系要被修改」第二个参数传给add的是「要被绑进来的值」。两者都可以是完整实体、实体 id或实体 id map复合主键场景甚至可以是数组。await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) // 也可以直接写 .of(post.id) 甚至 .of(1) .add(category) // 也可以直接写 .add(category.id) 甚至 .add(3).of(1).add(3)这样用「纯 id 绑定」是完全合法的因为绑定过程只关心主键值不需要对象本身await dataSource.createQueryBuilder().relation(Post, categories).of(1).add(3)底层行为因关系类型而异RelationUpdater.update()many-to-manyowner 侧RelationUpdater读取关系对应的 junction 表元数据relation.junctionEntityMetadata把of方与 value 方的主键值组装成连接表记录随后执行一条批量INSERT ... INTO junction_table除 Oracle / SAP 因驱动限制改为逐条插入外其余数据库均为单次批量插入。one-to-manyinverse 侧本质是把「子表外键列」指向父实体主键因此执行的是UPDATE inverse_entity_table SET join_column :parentId WHERE id IN (...)。值得注意的是源码对add和set都做了关系类型守卫——若对many-to-one或one-to-one调用add会抛出 TypeORMError 并提示改用它对应的.set()RelationQueryBuilder.add()。从关系中移除实体remove移除与添加的调用方式完全对称。它会解绑指定关系但不会删除实体本身// 从给定 post 上移除 category await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) // 传 post id 同样可行 .remove(category) // 传 category id 同样可行remove同样只适用于many-to-many与one-to-many对many-to-one/one-to-one调用会抛错并提示应使用.set(null)RelationQueryBuilder.remove()。底层行为RelationRemover.remove()many-to-many从 junction 表执行DELETEWHERE 条件按 owner 列 inverse 列的笛卡尔组合精确拼出即只删掉「这一对」的绑定不影响其它行one-to-many对被移除的子实体执行UPDATE将指向父实体的外键列置为NULL而不是删除子实体行。替换关系的单一目标set 与 set(null)many-to-many、one-to-many面向「集合」用 add / remove而one-to-one与many-to-one的关联是「单个对象」应当用set来赋值// 设置某个 post 的 category await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) // 传 post id 同样可行 .set(category) // 传 category id 同样可行想解除关系置空只需把null交给set// 解除某个 post 与 category 的关联 await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) // 传 post id 同样可行 .set(null)在源码层面对应三种不同形态RelationUpdater.update()many-to-one / one-to-oneowner 侧直接把外键列值 UPDATE 到父表记录上——UPDATE entity SET join_column :value WHERE id IN (:of)one-to-one非 owner/ one-to-many 且 value 为 null走「清空子表指向」的逻辑把 inverse 侧 join column 批量置 NULLone-to-one非 owner/ one-to-many 且 value 非 null更新子表将 inverse join column 指向.of()指定的父实体。源码还内置了复合 join column 的校验当关系含多个 join column 时若传入 value 不是对象或键数量不足会抛出错误并提示应使用.set({ firstName: ..., lastName: ... })这类 id mapRelationQueryBuilder.set()。此外set/add前都会检查.of()是否已调用未调用会直接报错「Entity whose relation needs to be set is not set」。复合主键与多列关联以 id map 传参当实体使用复合主键时不能只传单一 id必须把主键值组织成键值映射传给.of()与add/remove/set。官方文档给出的完整示例await dataSource .createQueryBuilder() .relation(Post, categories) .of({ firstPostId: 1, secondPostId: 3 }) .add({ firstCategoryId: 2, secondCategoryId: 4 })同理loadMany/loadOne在复合主键下也有约束若.of()只传单个裸值而目标实体含多个主键列RelationQueryBuilder 会抛出「Cannot load entity because only one primary key was specified…」错误RelationQueryBuilder.loadMany()。反过来如果实体只有一个主键列传入裸 id 也会在加载前自动通过primaryColumns[0].createValueMap(of)包装成合法的 value map。按需加载关联数据loadMany 与 loadOne关系查询构建器不仅能「写」关系还能在不需要全量 find 的情况下「读」出某条记录下的关联实体。官方文档的场景Post有many-to-many的categories和many-to-one的user先查主体再分别加载两类关联const post await dataSource.manager.findOneBy(Post, { id: 1, }) post.categories await dataSource .createQueryBuilder() .relation(Post, categories) .of(post) // 传 post id 同样可行 .loadMany() post.author await dataSource .createQueryBuilder() .relation(Post, user) .of(post) // 传 post id 同样可行 .loadOne()语义约定非常直观loadMany()针对集合型关系many-to-many、one-to-many、一对一的 inverse 侧返回关联实体数组RelationQueryBuilder.loadMany()loadOne()针对单个对象型关系many-to-one、one-to-one 的 owner 侧其实现本质是取loadMany()结果的第一项RelationQueryBuilder.loadOne()。加载时构建器委托给dataSource.relationLoader即 RelationLoader。RelationLoader 会按关系类型分派many-to-one/one-to-one owner 走「根据父记录 JOIN 查出目标」的路径one-to-many/one-to-one 非 owner 走反向查询路径而 many-to-many含 owner 与非 owner 两侧则经由 junction 表拼接出目标实体数据。一次性做多个操作addAndRemove实际业务中常常是「这次请求既想删掉旧关联、又要挂上新关联」。RelationQueryBuilder 为此提供了便捷的组合方法addAndRemove(added, removed)内部依次调用remove(removed)再add(added)RelationQueryBuilder.addAndRemove()两个参数同样都支持实体 / id / id map / 数组。数组批量支持add、remove以及addAndRemove中的两个参数都支持传数组意味着可以用一条链式调用完成多次绑定/解绑。源码对空数组做了短路保护Array.isArray(value) value.length 0时直接返回不产生任何 SQL见 RelationQueryBuilder.add() 与 remove 对应逻辑。底层实现从 RelationUpdater / RelationRemover 看真正执行的 SQL抛开 API 层看两个执行器能帮你更准确地预判每条调用会落成什么样的 SQL。add与set最终都汇聚到 RelationUpdater.update()其分支判断顺序非常清晰.of()目标关系类型实际执行父实体many-to-many对 junction 表批量INSERT组合 of 与 value 的笛卡尔积Oracle / SAP 逐条插父实体one-to-manyUPDATE子表外键列 父主键WHERE id IN (value)子实体作为 relation 目标时one-to-one 非 owner / one-to-many批量将 inverse join column 置为.of()或置NULLremove全部落在 RelationRemover.remove()many-to-many 直接对 junction 表 DELETE按 of 与 value 的每一组合逐一拼 AND 条件再用 OR 连接one-to-many 则是对子表做 UPDATE 把外键置 NULL。注意一个细微差别one-to-many 的 remove 并不会删子实体本身只是解绑外键置 NULL——如果你的外键列声明了NOT NULL那么 one-to-many 的 remove 将无法执行成功这点在建模时务必留意。测试用例佐证仓库在 test/functional/query-builder/relational/ 目录下为整套关系查询构建器提供了完备的功能测试可按关系类型对照with-many/query-builder-relational-add-remove-many-to-many.test.ts验证多对多下 add / remove 绑定生效且不影响同表其它记录的关联with-many/query-builder-relational-add-remove-many-to-many-inverse.test.ts验证从非 owner 一侧操作多对多with-many/query-builder-relational-add-remove-one-to-many.test.ts一对多场景的绑定与解绑with-many/query-builder-relational-load-many.test.ts验证loadMany返回的关联数组与预期一致with-one/query-builder-relational-set-many-to-one.test.ts、with-one/query-builder-relational-set-one-to-one.test.ts、with-one/query-builder-relational-set-one-to-one-inverse.test.ts验证 set 在 many-to-one / one-to-one含 inverse 侧下的赋值行为with-one/query-builder-relational-load-one.test.ts验证loadOne的单对象加载。这些测试跑在多种数据库连接之上且断言风格统一先save若干种子实体随后调用.relation(...).of(...).add/remove(...)最后通过findOneOrFail带relations重新加载并校验关联是否精确变更例如 query-builder-relational-add-remove-many-to-many.test.ts可直接作为你理解正确用法与预期结果的参照。使用建议与注意事项小结API 选择速查many-to-many / one-to-many 用add/removemany-to-one / one-to-one 用set置空传null。选错 API 时源码会抛出明确报错可按提示快速纠正。能传 id 就不传实体绑定与解绑只依赖主键直接用 id 可省掉一次实体加载进一步提升性能。复合主键必须用 id map单值传入在加载与多列 join column 场景下都会被源码拒绝并给出提示。它只改关系不改实体本身add / remove 不会插入或删除实体记录one-to-many 的 remove 只会置 NULL 外键实体级增删请走 repository 的save/remove。高基数关系如上万条关联是它的主场关系查询构建器的批量 INSERT / 精确 DELETE 天然规避了「先全量加载再保存」的巨额成本让集合型关系在高并发、大数据量下依然可控。若想了解关系模型如何定义join column、junction 表等可进一步阅读关系查询构建器之外的相关文档与源码本文聚焦的写操作入口统一从dataSource.createQueryBuilder().relation(...)发起与数据源初始化方式无关任何已建立的 DataSource 均可直接使用。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价