第 177 章:实体关系(一/多对多)
学习目标
- 掌握一对一、一对多、多对多关系
- 学会级联操作
- 理解延迟加载与渴求加载
- 学会关联查询
一、关系总览
二、一对多 / 多对一
2.1 定义
typescript
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// 用户拥有的文章(一对多)
@OneToMany(() => Post, post => post.author, { cascade: true })
posts: Post[];
}typescript
// post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
// 文章作者(多对一)
@ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'authorId' }) // 外键列名
author: User;
@Column()
authorId: number; // 外键
}2.2 保存
typescript
const user = new User();
user.name = 'Tom';
const post1 = new Post();
post1.title = '第一篇';
const post2 = new Post();
post2.title = '第二篇';
user.posts = [post1, post2];
await userRepo.save(user); // 级联保存
// 一条 SQL 同时插入 user 和 2 条 post三、一对一
3.1 定义
typescript
// user.entity.ts
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;typescript
// profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
// Profile 持有外键
@OneToOne(() => User, user => user.profile, { onDelete: 'CASCADE' })
@JoinColumn()
user: User;
@Column()
userId: number;
}3.2 使用
typescript
const user = await userRepo.findOne({
where: { id: 1 },
relations: { profile: true }, // 关联查询
});
// user.profile.bio四、多对多
4.1 定义
typescript
// post.entity.ts
@ManyToMany(() => Tag, tag => tag.posts, { cascade: true })
@JoinTable({ // 关系拥有方,自动建中间表
name: 'post_tags',
joinColumn: { name: 'postId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'tagId', referencedColumnName: 'id' },
})
tags: Tag[];typescript
// tag.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, post => post.tags)
posts: Post[];
}4.2 操作
typescript
// 给文章加标签
const post = await postRepo.findOne({ where: { id: 1 }, relations: ['tags'] });
const tag = await tagRepo.findOne({ where: { name: 'tech' } });
post.tags.push(tag);
await postRepo.save(post);
// 查询所有标签为 tech 的文章
const posts = await postRepo
.createQueryBuilder('post')
.innerJoinAndSelect('post.tags', 'tag', 'tag.name = :name', { name: 'tech' })
.getMany();五、级联操作(Cascade)
typescript
@OneToMany(() => Post, post => post.author, {
cascade: true, // insert / update 级联
// cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover']
})
posts: Post[];
// cascade 行为
// insert:保存父时自动保存子
// update:更新父时自动更新子
// remove:删除父时自动删除子
// soft-remove / recover:软删除恢复typescript
// 保存(级联)
await userRepo.save(user); // 自动保存 posts
// 删除(级联)
await userRepo.remove(user); // 自动删除 posts
// 不级联:用普通 save
await userRepo.save({ ...user, posts: undefined });六、渴求加载(Eager Loading)
6.1 关系级
typescript
@Entity()
export class User {
@OneToMany(() => Post, post => post.author, {
eager: true, // 总是自动加载
})
posts: Post[];
}6.2 查询级(更灵活)
typescript
// 简单 relations
const user = await userRepo.findOne({
where: { id: 1 },
relations: { posts: true, profile: true },
});
// 嵌套 relations
const user = await userRepo.findOne({
where: { id: 1 },
relations: {
posts: {
tags: true,
comments: true,
},
profile: true,
},
});七、QueryBuilder 关联查询
7.1 leftJoinAndSelect
typescript
const user = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.id = :id', { id: 1 })
.getOne();
// SELECT user.*, post.*
// FROM users user
// LEFT JOIN posts post ON post.authorId = user.id
// WHERE user.id = 17.2 多级关联
typescript
const users = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.tags', 'tag')
.leftJoinAndSelect('post.comments', 'comment')
.getMany();7.3 条件过滤
typescript
const users = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect(
'user.posts',
'post',
'post.published = :published',
{ published: true },
)
.where('user.role = :role', { role: 'admin' })
.getMany();八、加载策略对比
| 策略 | SQL 数 | 性能 | 适用 |
|---|---|---|---|
| relations | 1 | 优 | 数据量小 |
| QueryBuilder | 1 | 优 | 复杂关联 |
| lazy: true | N | 差 | 极少用 |
| eager: true | 1 | 自动 | 总是要的数据 |
九、避免 N+1 问题
typescript
// ❌ 错误:每次访问 author 都查一次
const posts = await postRepo.find();
for (const post of posts) {
console.log(post.author.name); // N+1!
}
// ✅ 正确:一次 JOIN
const posts = await postRepo.find({ relations: ['author'] });
posts.forEach(p => console.log(p.author.name));十、关系操作实战
10.1 添加关联
typescript
// 给用户追加一篇文章
const user = await userRepo.findOne({ where: { id: 1 }, relations: ['posts'] });
const newPost = postRepo.create({ title: '新文章' });
user.posts.push(newPost);
await userRepo.save(user); // 级联保存10.2 移除关联
typescript
// 删除中间表的关联(多对多)
await postRepo
.createQueryBuilder()
.relation(Post, 'tags')
.of(postId)
.remove(tagId);
// 设置关联(替换)
await postRepo
.createQueryBuilder()
.relation(Post, 'tags')
.of(postId)
.set([tagId1, tagId2]);
// 解除所有
await postRepo
.createQueryBuilder()
.relation(Post, 'tags')
.of(postId)
.set(null);十一、本章小结
| 关系 | 装饰器 | 谁持有外键 |
|---|---|---|
| 一对一 | @OneToOne + @JoinColumn | 一边 |
| 一对多 | @OneToMany + @ManyToOne | 多边 |
| 多对多 | @ManyToMany + @JoinTable | 一边 |
动手练习
- 实现 User (1) -> Post (N) -> Tag (N:M) 三层关联
- 实现一个接口返回用户的全部文章和标签
- 用 cascade 实现自动保存
- 用 relations 解决 N+1 问题