实体关系全解析
原创2026/3/18大约 6 分钟
实体关系是 TypeORM 映射数据库表间关联的核心能力,对应数据库的外键关系,支持一对一、一对多/多对一、多对多三种核心关系,同时提供级联操作、懒加载/急加载等扩展能力。
一、关系基础概念
1. 关系方向
- 单向关系:仅一方实体声明关联(如 User → Profile,仅 User 知道 Profile 存在);
- 双向关系:双方实体都声明关联(如 User ↔ Profile,互相知道对方存在),便于从任意一方查询关联数据。
2. 外键归属
外键默认创建在“多”的一方(一对多)或从属方(一对一),也可通过配置自定义外键名称和字段。
二、一对一(One-to-One)
一对一关系表示两个实体间一一对应(如 User ↔ Profile),分为单向和双向:
1. 单向一对一
仅主实体(User)声明关联 Profile,外键存储在 Profile 表:
// User.ts(主实体)
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from "typeorm";
import { Profile } from "./Profile";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
// 声明一对一关联 Profile,JoinColumn 表示当前实体是外键拥有方
@OneToOne(() => Profile)
@JoinColumn() // 必须加,否则 TypeORM 不知道外键在哪一方
profile: Profile;
}
// Profile.ts(从实体)
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: string;
@Column()
address: string;
// 无关联声明,单向关系中 Profile 不知道 User 存在
}2. 双向一对一
双方都声明关联,可从任意一方查询对方:
// User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from "typeorm";
import { Profile } from "./Profile";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@OneToOne(() => Profile, (profile) => profile.user) // 反向关联 profile.user
@JoinColumn() // 外键仍在 Profile 表
profile: Profile;
}
// Profile.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm";
import { User } from "./User";
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: string;
@Column()
address: string;
// 反向关联 User,无需加 JoinColumn(外键已在 User 侧声明)
@OneToOne(() => User, (user) => user.profile)
user: User;
}3. 自定义外键
通过 JoinColumn 配置自定义外键名称和字段:
@OneToOne(() => Profile)
@JoinColumn({
name: "profile_id", // 外键字段名(默认:关联实体名+Id,如 profileId)
referencedColumnName: "id", // 关联实体的主键(默认 id)
nullable: true, // 外键是否允许为空(默认 false)
unique: true // 一对一必须唯一(默认 true)
})
profile: Profile;三、一对多/多对一(One-to-Many/Many-to-One)
最常用的关系类型(如 User → 多个 Order,多个 Order 属于一个 User),外键存储在“多”的一方(Order 表):
1. 双向一对多/多对一
// User.ts(一的一方)
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Order } from "./Order";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
// 一对多关联 Order,反向关联 order.user
@OneToMany(() => Order, (order) => order.user)
orders: Order[];
}
// Order.ts(多的一方)
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { User } from "./User";
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderNo: string;
@Column()
amount: number;
// 多对一关联 User,JoinColumn 声明外键在 Order 表(必须加)
@ManyToOne(() => User, (user) => user.orders)
@JoinColumn({ name: "user_id" }) // 自定义外键名
user: User;
}2. 单向多对一
仅“多”的一方(Order)声明关联 User,User 不知道 Order 存在:
// Order.ts
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderNo: string;
@ManyToOne(() => User)
@JoinColumn()
user: User;
}
// User.ts 无关联声明
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
}四、多对多(Many-to-Many)
多对多关系表示两个实体间多对多关联(如 User ↔ Role),TypeORM 会自动创建中间表,也可自定义中间表:
1. 基础多对多(自动生成中间表)
// User.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm";
import { Role } from "./Role";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
// 多对多关联 Role,JoinTable 表示当前实体是中间表拥有方
@ManyToMany(() => Role, (role) => role.users)
@JoinTable() // 必须加,否则 TypeORM 不知道中间表归属
roles: Role[];
}
// Role.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { User } from "./User";
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
code: string;
@Column()
name: string;
// 反向关联 User
@ManyToMany(() => User, (user) => user.roles)
users: User[];
}自动生成的中间表名:user_role(两个实体名小写拼接),字段:userId、roleId。
2. 自定义中间表
通过 JoinTable 配置中间表名称、字段名,或自定义中间表实体(含额外字段):
// 方式1:简单自定义中间表名称/字段
@ManyToMany(() => Role, (role) => role.users)
@JoinTable({
name: "sys_user_role", // 中间表名
joinColumn: {
name: "user_id", // 当前实体(User)在中间表的字段名
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "role_id", // 关联实体(Role)在中间表的字段名
referencedColumnName: "id",
},
})
roles: Role[];
// 方式2:自定义中间表实体(含额外字段,如创建时间)
// UserRole.ts(中间表实体)
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm";
import { User } from "./User";
import { Role } from "./Role";
@Entity("sys_user_role")
export class UserRole {
@PrimaryGeneratedColumn()
id: number;
@Column()
createTime: Date;
@ManyToOne(() => User)
@JoinColumn({ name: "user_id" })
user: User;
@ManyToOne(() => Role)
@JoinColumn({ name: "role_id" })
role: Role;
}
// User.ts 改造为两个一对多
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@OneToMany(() => UserRole, (userRole) => userRole.user)
userRoles: UserRole[];
}
// Role.ts 改造为两个一对多
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
code: string;
@OneToMany(() => UserRole, (userRole) => userRole.role)
userRoles: UserRole[];
}五、级联操作(cascade)
级联操作允许操作主实体时,自动同步关联实体(如保存 User 时自动保存 Profile),避免手动操作关联数据:
// 一对一关联配置级联
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true, // 所有操作(save/remove)都级联
// 或精准配置:cascade: ["insert", "update", "remove"]
})
@JoinColumn()
profile: Profile;
// 使用示例:保存 User 时自动保存 Profile
const profile = new Profile();
profile.gender = "男";
profile.address = "北京市";
const user = new User();
user.username = "zhangsan";
user.profile = profile;
await userRepository.save(user); // 自动保存 User + Profile常用级联配置值:
true:等价于["insert", "update", "remove", "soft-remove", "recover"];["insert"]:仅新增主实体时级联新增关联实体;["update"]:仅更新主实体时级联更新关联实体;["remove"]:仅删除主实体时级联删除关联实体(谨慎使用,避免误删)。
六、懒加载 vs 急加载
1. 急加载(Eager Loading)
查询主实体时,自动加载关联实体(通过 eager: true 配置):
// User.ts 配置急加载 Profile
@OneToOne(() => Profile, (profile) => profile.user, {
eager: true // 查 User 时自动查 Profile
})
@JoinColumn()
profile: Profile;
// 使用:查询 User 时自动包含 Profile
const user = await userRepository.findOne({ where: { id: 1 } });
console.log(user.profile); // 直接可用,无需额外查询2. 懒加载(Lazy Loading)
查询主实体时,关联实体返回 Promise,需手动触发加载(适合关联数据大、不常用的场景):
// User.ts 配置懒加载(关联属性类型为 Promise)
@OneToOne(() => Profile, (profile) => profile.user)
@JoinColumn()
profile: Promise<Profile>; // 注意:类型是 Promise<Profile>
// 使用:需 await 加载关联数据
const user = await userRepository.findOne({ where: { id: 1 } });
const profile = await user.profile; // 手动加载 Profile
console.log(profile);3. 手动加载关联(推荐)
通过 relations 或 QueryBuilder 手动指定加载的关联,兼顾灵活性和性能:
// 方式1:find 方法通过 relations 加载
const user = await userRepository.findOne({
where: { id: 1 },
relations: ["profile", "orders"] // 加载 Profile 和 Orders 关联
});
// 方式2:QueryBuilder 加载(更灵活)
const user = await userRepository
.createQueryBuilder("u")
.leftJoinAndSelect("u.profile", "p")
.leftJoinAndSelect("u.orders", "o")
.where("u.id = :id", { id: 1 })
.getOne();七、关键注意事项
- 外键约束:TypeORM 自动创建外键约束,删除关联数据前需先删除外键引用,或配置
onDelete: "CASCADE"(删除主实体时自动删除从实体); - 性能优化:
- 避免全局急加载(
eager: true),优先手动加载关联; - 多对多关联尽量分页查询,避免一次性加载大量数据;
- 避免全局急加载(
- 双向关系:反向关联的回调函数(如
(profile) => profile.user)必须正确指向对方的关联属性,否则关系不生效; - 中间表操作:自定义中间表实体时,需通过中间表 Repository 操作关联(如新增用户角色需保存 UserRole 实体)。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。