尧图精选

TypeORM 多对一 / 一对多关联(Many-to-One / One-to-Many)实战指南

🕒 发布时间:2026/9/10 1:41:44 📁 来源:尧图网络
TypeORM 多对一 / 一对多关联Many-to-One / One-to-Many实战指南【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm关系型数据库的实体建模中“多对一 / 一对多”是最常见、也最实用的一类关联一个User拥有多张Photo而每张Photo只属于一个User。本篇技术指南基于 TypeORM 官方 relations 文档中的多对一 / 一对多章节展开完整讲解ManyToOne与OneToMany的配对声明、外键落点、级联保存、查询加载、关系 ID 用法与联合外键composite key细节并对照 关系元数据与装饰器源码让你既能直接照抄运行示例也能理解其底层原理。读完你将掌握在多对一/一对多场景下的建表结果、四种保存姿势、find/QueryBuilder/eager 三种加载方式以及JoinColumn自定义外键的全部用法。关联语义与实体声明多对一 / 一对多关系描述的是A 包含 B 的多个实例而 B 只包含 A 的一个实例。以User与Photo为例一个用户User可以拥有多张照片Photo一张照片只属于唯一一个用户。“多”方使用ManyToOne在Photo实体上声明多对一关联它指向Userimport { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from typeorm import { User } from ./User Entity() export class Photo { PrimaryGeneratedColumn() id: number Column() url: string ManyToOne(() User, (user) user.photos) user: User }“一”方使用OneToMany在User实体上声明反向的一对多关联import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from typeorm import { Photo } from ./Photo Entity() export class User { PrimaryGeneratedColumn() id: number Column() name: string OneToMany(() Photo, (photo) photo.user) photos: Photo[] }两个装饰器的回调函数参数都是() Entity形式的延迟引用函数lazy arrow function用于规避循环 import 导致的死循环。第二个参数互为“反向引用”ManyToOne的第二参数写的是反向属性名user.photosOneToMany的第二参数写的是子实体上对应的多对一属性photo.user。从源码看装饰器的职责从 ManyToOne 装饰器源码 可见装饰器本体并不做任何数据库操作它只负责把一段RelationMetadataArgs记录注册进全局的 metadata storagegetMetadataArgsStorage().relations.push({ target: object.constructor, propertyName: propertyName, relationType: many-to-one, isLazy: isLazy, type: typeFunctionOrTarget, inverseSideProperty: inverseSideProperty, options: options, } as RelationMetadataArgs)其中关键的两点relationType: many-to-one/one-to-many——元数据构建器靠这个字段区分关联类型决定外键该建在哪张表、JOIN 方向如何。参见 OneToMany 装饰器源码。装饰器还会自动探测懒加载如果属性的design:type反射元数据是Promise会自动把isLazy置为true无需显式传lazy: true。谁建外键两张实体的关键差异理解“外键落在哪一侧”是多对一 / 一对多建模的核心。文档给出的规则非常清晰务必牢记可以省略JoinColumn多对一 / 一对多关系不需要显式JoinColumn也会自动生成外键列这一点与OneToOne不同后者拥有方必须写。OneToMany不能脱离ManyToOne独立存在因为外键一定建立在“多”方的表里必须有ManyToOne来承载它反向则不需要——你完全可以只定义ManyToOne在对应实体上不写OneToMany。如果只关心“这张照片属于谁”单独写ManyToOne即可。哪里写了ManyToOne哪里就产生“relation id”外键列。对应本示例ManyToOne写在Photo上所以外键userId落在photo表而不是user表。生成的表结构同步synchronize: true或执行schema:sync后生成的两张表如下------------------------------------------------------- | photo | ------------------------------------------------------- | id | int | PRIMARY KEY AUTO_INCREMENT | | url | varchar(255) | | | userId | int | FOREIGN KEY | ------------------------------------------------------- ------------------------------------------------------- | user | ------------------------------------------------------- | id | int | PRIMARY KEY AUTO_INCREMENT | | name | varchar(255) | | -------------------------------------------------------注意photo表上由 TypeORM 自动生成的userId列列名 关系属性名user 被引用主键列名id即userId。数据库层面它是一条指向user.id的FOREIGN KEY约束。保存关联数据两种“由谁主导”的写法方式一先保存子实体再从父侧挂接先分别保存两张Photo再创建User把照片数组赋给user.photos最后保存Userconst photo1 new Photo() photo1.url me.jpg await dataSource.manager.save(photo1) const photo2 new Photo() photo2.url me-and-bears.jpg await dataSource.manager.save(photo2) const user new User() user.name John user.photos [photo1, photo2] await dataSource.manager.save(user)这段代码背后执行了两步操作先INSERT两张照片此时它们还没有外键值再INSERT用户随后 TypeORM 用UPDATE把userId写回两张照片。共产生 4 条 SQL2 次 INSERT 1 次 INSERT 1 次 UPDATE。方式二先保存父实体从子侧挂接反过来也可以先保存User再把同一user实例赋给每张Photo.user后分别保存const user new User() user.name Leo await dataSource.manager.save(user) const photo1 new Photo() photo1.url me.jpg photo1.user user await dataSource.manager.save(photo1) const photo2 new Photo() photo2.url me-and-bears.jpg photo2.user user await dataSource.manager.save(photo2)由于先保存了用户、拿到了它的主键照片的INSERT语句里就可以直接带上外键值无需多余的 UPDATESQL 数量更少。两种方式业务语义等价。哪种更优取决于应用场景从子侧主导方式二SQL 更干净且无需关系处于持久化状态即可写入外键从父侧主导方式一在一次性批量建立树形/一对多结构时更直观。方式三配合 cascades 一次save搞定对子侧多对一关系开启级联后可以只保存一次。例如在User.photos上配置OneToMany(() Photo, (photo) photo.user, { cascade: true, }) photos: Photo[]然后在Photo上的ManyToOne同时开启级联ManyToOne(() User, (user) user.photos, { cascade: true, }) user: User此时再执行方式一的保存流程TypeORM 会自动按拓扑顺序先插入User、再插入两张Photo一次save调用即可完成全部持久化不需要手动逐个save子实体。级联语义详见 relations 总览文档的 Cascades 章节其中还特别提醒级联虽然方便但“能力越大责任越大”。它可能把不该入库的对象悄悄写进数据库也可能带来安全与 bug 隐患。精细控制时更推荐使用cascade: [insert]、cascade: [update]这类数组形式或干脆关闭级联、显式管理。级联可取值在 RelationOptions 源码 中定义cascade?: boolean | (insert | update | remove | soft-remove | recover)[]cascade: true等价于开启以上全部级联操作cascade: [insert, update]只在新对象插入与已存在对象更新时自动同步默认false不级联需显式保存每一侧实体。另外注意cascade: [remove]只对已加载到内存的关系集合生效删除父实体前需先通过relations加载子实体否则子行不会被级联删除。参考 relations 总览文档中的 remove 说明。方式四relation id 直接赋值有些场景你不希望加载整个关联实体只关心外键值本身。TypeORM 会为ManyToOne属性生成一个对应的 “relation id” 属性命名规则为关系属性名 Id首字母大小写取决于属性声明。沿用本例Photo上会存在userId属性。可直接赋值保存const photo new Photo() photo.url me.jpg photo.userId 42 // 直接写入外键不需要加载整个 User await dataSource.manager.save(photo)在加载时若只想拿到外键值而不想 join 关联表也可以在find的select中直接选取 relation id 列或在查询结果上访问photo.userId。这一技巧在 Relations FAQ 的 “How to use relation id without joining relation?” 小节有专门阐述常用于避免大对象加载、提升查询性能。加载关联数据find options、QueryBuilder 与 eager由于普通属性不设置任何选项时多对一/一对多关系默认不会自动加载需要显式指定。方式一find*relationsFindOptions的relations对象键对应关系属性名置为true表示加载// 从一方加载其下的多方 const userRepository dataSource.getRepository(User) const users await userRepository.find({ relations: { photos: true, }, }) // 从多方反向加载其所属的一方 const photoRepository dataSource.getRepository(Photo) const photos await photoRepository.find({ relations: { user: true, }, })第一条查询会生成LEFT JOIN把每张照片挂到user.photos数组第二条查询把每个photo.user填充为单个User对象。返回的users[0].photos类型是Photo[]photos[0].user类型是User——两端均可遍历读取任意一个方向的加载都会把两侧关联对象正确反填。同样地findOne、findOneBy等查找方法都支持relations若要同时按条件过滤子表可结合where: { photos: { … } }或直接改用下面的 QueryBuilder。方式二QueryBuilderleftJoinAndSelectQueryBuilder适合动态拼接、分页、组合 where 条件等复杂查询// 一次查询加载用户及其所有照片 const users await dataSource .getRepository(User) .createQueryBuilder(user) .leftJoinAndSelect(user.photos, photo) .getMany() // 反向加载照片时把所属用户一并带出 const photos await dataSource .getRepository(Photo) .createQueryBuilder(photo) .leftJoinAndSelect(photo.user, user) .getMany()关键点leftJoinAndSelect(user.photos, photo)第一个参数是目标实体的关系路径从当前查询主体出发第二个参数是该关系的别名后续where、orderBy、select里都引用此别名若仅想过滤而不取关联数据可改用innerJoin/leftJoin不带AndSelect避免多余字段进入结果想要“只加载那些至少有一张照片的用户”用innerJoinAndSelect替代leftJoinAndSelect即可若在ManyToOne上配置了nullable: false加载该关系时 TypeORM 会改用INNER JOIN而不是LEFT JOIN——因为数据库已保证关联实体必然存在。见 relations 总览文档的 nullable 说明。方式三eager 自动加载附 QueryBuilder 限制若在关系上开启 eager 加载那么只要使用find*系列方法加载拥有方实体该关系就永远会被自动加载无需任何relations指定ManyToOne(() User, (user) user.photos, { eager: true, // 每次 find Photo 都会自动带出 user }) user: User需要特别警惕两条边界eager 只能开在关系的一端——不能同时在ManyToOne与反向的OneToMany上都设eager: true否则会因双向自动加载造成循环加载。最佳实践是只把eager开在真正“高概率被访问”的一侧通常是把ManyToOne设为 eager加载单个子实体时即获知所属父实体。QueryBuilder会忽略 eager 配置——一旦改用createQueryBuilder()eager 关系不会被自动加载必须显式写leftJoinAndSelect。这与上面ManyToOne装饰器源码中把关系元数据写入 storage 的机制一致eager 是find*执行阶段读元数据后统一 join 的而 QueryBuilder 的 SQL 完全由用户手写 join 决定。完整的 eager 示例可参见 relations 总览文档 及仓库中的 basic-eager-relations 测试。小结对比find*relations与 QueryBuilderleftJoinAndSelect都能精确控制加载eager 牺牲控制换取省心但注意它只对find*生效QueryBuilder 必须手动 join。更多配置ManyToOne各选项速查ManyToOne/OneToMany的第三个参数是一个RelationOptions对象。除cascade、eager、nullable外常用选项还有完整定义见 RelationOptions 源码选项可选值默认值说明onDeleteRESTRICT \| CASCADE \| SET NULL默认RESTRICT被引用的父行删除时外键如何动作。CASCADE会让数据库连带删除该外键行SET NULL需配合nullable: trueonUpdateCASCADE \| SET NULL \| RESTRICT \| NO ACTION等父行主键更新时的联动行为nullableboolean默认true外键列是否可空。置false后生成非空外键列同时影响加载 JOIN 策略变 INNER JOINcreateForeignKeyConstraintsboolean默认true是否真正创建数据库级外键约束。仅对多对一和拥有侧一对一有效设为false时只建列不建约束适合已有约束或跨库场景lazyboolean默认false懒加载访问时返回Promise。属性类型声明为Promise时可自动识别eagerboolean默认false如上节所述persistenceboolean默认true关闭后可避免每次save对关系做额外查询此时只能从反向或 RelationQueryBuilder 修改关系orphanedRowActionnullify \| delete \| soft-delete \| disable默认nullify父实体在级联保存时“丢弃”了数据库中仍存在的子行时如何处理nullify置空外键、delete物理删除、soft-delete逻辑删除、disable保持不动此外外键约束可以被设置成可延迟校验deferrable该选项主要面向 PostgreSQL、better-sqlite3、SAP HANA 等支持DEFERRABLE约束的驱动。自定义外键JoinColumn与复合外键JoinColumn在多对一关系中是可选的但当你需要自定义外键列名或引用非主键列时就需要显式写上它。命名自定义ManyToOne((type) Category, (category) category.posts) JoinColumn({ name: cat_id }) category: Category数据库列名将由默认的categoryId改为cat_id。引用其他列ManyToOne((type) Category) JoinColumn({ referencedColumnName: name }) category: Category此时外键引用Category.name而非Category.id生成的列名为categoryName属性名 被引用列名。这对“以自然键关联”的业务场景很有用但注意被引用列必须具有唯一性以保证引用语义正确。复合外键composite join columns当事先约定用多个列共同标识关联目标时JoinColumn接受数组。注意复合连接列默认不引用主键你必须为每一列显式给出referencedColumnNameManyToOne((type) Category) JoinColumn([ { name: category_id, referencedColumnName: id }, { name: locale_id, referencedColumnName: locale_id }, ]) category: Category对应的实体声明ManyToOne(() Category, (category) category.posts)且Category需要以id与locale_id组成复合主键。仓库中有大量此类测试佐证例如 multiple-primary-keys-many-to-one 测试覆盖了多主键目标实体上的多对一关联与反向一对多关联。兼容性提示使用复合JoinColumn/JoinTable时TypeORM 会自动按被引用实体的主键顺序对连接列排序以保证 MySQL、MSSQL、SAP HANA 等要求外键按主键索引顺序引用主键列的数据库能够正常建表。该行为同样作用于多对多关系。自引用结构树形建模的延伸应用多对一/一对多的经典变体是自引用关系self-referencing常用于类别树、评论回复树等层级数据。在 Relations FAQ 中有专门示例让Category同时声明parentCategoryManyToOne指向自身与childCategoriesOneToMany同样指向自身Entity() export class Category { PrimaryGeneratedColumn() id: number Column() title: string ManyToOne((type) Category, (category) category.childCategories) parentCategory: Category OneToMany((type) Category, (category) category.parentCategory) childCategories: Category[] }外键parentCategoryId落在本表形成“邻接表”adjacency list模式。若要查询整棵子树可再配合TreeRepository或Tree系列装饰器materialized path / nested set / closure table那是另一个话题此处不展开。常见陷阱与最佳实践清单结合文档与源码整理出多对一/一对多建模中最容易踩的坑忘记配对即报错OneToMany的反向实体如果没有对应的ManyToOne并正确指回photo.userTypeORM 构建元数据时会抛出错误。反向引用必须双向成对除非只写ManyToOne单侧。在“一”方写JoinColumn多对一/一对多的外键永远跟随ManyToOne的宿主。不要在OneToMany一侧试图放置连接列这不会生效——连接列控制权只在“多”方此点与OneToOne需要显式指定拥有方不同。懒加载与类型声明若属性类型写成PromiseUserTypeORM 反射到的design:type为Promise会自动启用懒加载访问await photo.user才触发查询。请确保属性类型与实际使用方式一致避免意外把普通关联变成懒加载或相反。eager 与 N1eager 虽然省心但会在find*时无条件多 JOIN。大批量列表查询建议使用 QueryBuilder 按需 join反过来在 QueryBuilder 中忘了leftJoinAndSelect则 eager 不会生效行为不一致极易混淆。删除孤儿行父实体级联保存时移除了原来挂接的子实体若外键列非空且orphanedRowAction为nullifyTypeORM 会改为直接删除该子行避免空值冲突。设置外键nullable: false前务必先想清楚此语义。小结本指南把 TypeORM 的多对一/一对多关系拆解为五步可执行的心智模型用ManyToOne声明“多”侧外键所在侧→ 用OneToMany声明“一”侧成对反向引用→ 按需配cascade/eager/nullable/onDelete→ 保存时选父侧主导或子侧主导 → 加载时在 find relations 与 QueryBuilder join 之间取舍。需要自定义列名、引用非主键或复合外键时把JoinColumn显式写出即可。文中全部行为都可在 TypeORM 源码与测试中交叉验证——相关 装饰器实现、关系选项接口 以及大量 relations 功能测试 是继续深入研读的最佳入口。【免费下载链接】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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →