TypeORM 入门教程之 `@OneToOne` 关系详解
@OneToOne
是 TypeORM 中用于定义实体间一对一关系的装饰器,适用于两个实体间存在严格唯一对应关系的场景(如用户与身份证、订单与发票)。以下是其核心用法和关键配置:
基础用法
1. 单向一对一关系
场景:仅在一个实体中定义对另一个实体的引用。
// User.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { Profile } from './Profile.entity';@Entity()
export class User {@PrimaryGeneratedColumn()id: number;@Column()name: string;// 单向关联(User 知道 Profile,但 Profile 不知道 User)@OneToOne(() => Profile)profile: Profile;
}// Profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';@Entity()
export class Profile {@PrimaryGeneratedColumn()id: number;@Column()bio: string;
}
数据库结果:
User
表会新增一个外键列(如profileId
),指向Profile
表的主键。
2. 双向一对一关系
场景:两个实体相互引用对方,需通过 @JoinColumn
指定关系所有者。
// User.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Profile } from './Profile.entity';@Entity()
export class User {@PrimaryGeneratedColumn()id: number;@Column()name: string;// 双向关联(User 是关系所有者)@OneToOne(() => Profile, (profile) => profile.user)@JoinColumn() // 必须指定,否则会报错profile: Profile;
}// Profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { User } from './User.entity';@Entity()
export class Profile {@PrimaryGeneratedColumn()id: number;@Column()bio: string;// 反向引用(非必须,但推荐以支持双向查询)@OneToOne(() => User, (user) => user.profile)user: User;
}
数据库结果:
- 外键列仅在关系所有者(
User
)的表中生成(如profileId
)。
关键配置
1. @JoinColumn
详解
- 作用:指定关系所有者,并定义外键列名。
- 必填性:在双向关系中,必须在关系所有者一侧添加。
- 自定义外键名:
@OneToOne(() => Profile, (profile) => profile.user) @JoinColumn({ name: 'custom_profile_id' }) // 自定义外键列名 profile: Profile;
2. 级联操作(Cascade)
通过 cascade
选项自动管理关联实体的生命周期:
@OneToOne(() => Profile, (profile) => profile.user, {cascade: true, // 自动保存/更新/删除关联实体onDelete: 'CASCADE', // 数据库级联删除(可选)
})
@JoinColumn()
profile: Profile;
3. 通过主键关联(类似 JPA @MapsId
)
场景:当关联实体的外键与主键相同时(如 Client.id = User.id
)。
// Client.entity.ts
import { Entity, PrimaryColumn, OneToOne, JoinColumn } from 'typeorm';
import { User } from './User.entity';@Entity()
export class Client {@PrimaryColumn() // 必须使用 @PrimaryColumn 而非 @PrimaryGeneratedColumnid: number;@OneToOne(() => User, { primary: true }) // primary: true 表示通过主键关联@JoinColumn({ name: 'id' }) // 外键列名与主键相同user: User;
}
查询操作
1. 加载关联数据
- 隐式加载(需配置
eager: true
):@OneToOne(() => Profile, (profile) => profile.user, { eager: true }) profile: Profile;
- 显式加载(推荐):
const user = await userRepository.find({relations: ['profile'], // 加载关联的 profile });
2. 查询构建器(QueryBuilder)
const user = await userRepository.createQueryBuilder('user').leftJoinAndSelect('user.profile', 'profile') // 左连接并选择 profile.where('user.id = :id', { id: 1 }).getOne();
完整示例
// User.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Profile } from './Profile.entity';@Entity()
export class User {@PrimaryGeneratedColumn()id: number;@Column()name: string;@OneToOne(() => Profile, (profile) => profile.user, {cascade: true,onDelete: 'CASCADE',})@JoinColumn()profile: Profile;
}// Profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { User } from './User.entity';@Entity()
export class Profile {@PrimaryGeneratedColumn()id: number;@Column()bio: string;@OneToOne(() => User, (user) => user.profile)user: User;
}// 使用示例
async function createUserWithProfile() {const profile = new Profile();profile.bio = 'TypeORM Expert';const user = new User();user.name = 'Alice';user.profile = profile; // 自动级联保存await userRepository.save(user);
}async function getUserWithProfile(id: number) {const user = await userRepository.findOne({where: { id },relations: ['profile'], // 加载关联数据});console.log(user?.profile?.bio); // 输出: TypeORM Expert
}
常见问题
-
外键列未生成:
- 检查是否在关系所有者一侧添加了
@JoinColumn
。 - 确保实体已正确同步到数据库(运行迁移或重新启动应用)。
- 检查是否在关系所有者一侧添加了
-
循环依赖:
- 避免在双向关系的装饰器回调中直接引用对方实体(如
@OneToOne(() => Profile, (profile) => profile.user)
是安全的,但循环导入模块会导致问题)。
- 避免在双向关系的装饰器回调中直接引用对方实体(如
-
性能优化:
- 对频繁查询的关联数据使用
eager: true
或@RelationId
装饰器。 - 对复杂查询使用
QueryBuilder
以减少 N+1 问题。
- 对频繁查询的关联数据使用