Skip to content
第 177 / 250 章Node⏱ 10 分钟阅读

第 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 = 1

7.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 数性能适用
relations1数据量小
QueryBuilder1复杂关联
lazy: trueN极少用
eager: true1自动总是要的数据

九、避免 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一边

动手练习

  1. 实现 User (1) -> Post (N) -> Tag (N:M) 三层关联
  2. 实现一个接口返回用户的全部文章和标签
  3. 用 cascade 实现自动保存
  4. 用 relations 解决 N+1 问题

推荐阅读


下一章:第 178 章:Repository 与查询构建器

本站基于 VitePress 构建 · 由 Codebook 团队维护