第十章 多对多关系
本章目标
- 理解多对多关系为什么需要"中间表",以及中间表的结构。
- 能用
@ManyToMany+@JoinTable建立双向多对多关系,并自定义中间表名和列名。 - 掌握两种增删关联的方式:改数组再
save、以及relation().add()/remove()直接操作中间表。
核心概念
学生 ↔ 课程:一个学生可以选多门课,一门课有多个学生选。这种关系没法用一张外键列表达(一行学生记不下多个课程 id,一行课程也记不下多个学生 id),所以数据库引入第三张表——中间表(选课记录表),每行就是一对 (学生id, 课程id)。
TypeORM 里:
- 两边实体都写
@ManyToMany; - 其中一边挂
@JoinTable(),表示"中间表由我负责",这边叫拥有方; - 中间表由 TypeORM 自动生成:两个外键列 + 联合主键,并且两个外键默认带
ON DELETE CASCADE(删文章/标签时自动清理中间表里的关联行)。
双向多对多标准写法
- post.ts
ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from "typeorm";
import { Tag } from "./Tag";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable({
name: "post_tag_relation",
joinColumn: { name: "post_id", referencedColumnName: "id" },
inverseJoinColumn: { name: "tag_id", referencedColumnName: "id" },
})
tags: Tag[];
}- Tag.ts
ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Post } from "./Post";
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}@JoinTable 自定义中间表
不指定时 TypeORM 会自动起一个超长的表名(类似 post_tags_tag)。建议显式命名:
ts
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable({
name: "post_tag_relation",
joinColumn: { name: "post_id", referencedColumnName: "id" },
inverseJoinColumn: { name: "tag_id", referencedColumnName: "id" },
})
tags: Tag[];| 选项 | 含义 |
|---|---|
name | 中间表名 |
joinColumn | 指向自己所在实体的外键列配置 |
inverseJoinColumn | 指向对方实体的外键列配置 |
多对对保存 只能用save 保存 关联关系
ts
import { AppDataSource } from "./data-source";
import { Tag } from "./entities/Tag";
import { Post } from "./entities/Post";
AppDataSource.initialize()
.then(async () => {
const manage = AppDataSource.manager;
// 先保存 Tag
const tag1 = await manage.save(Tag, { name: "tag1" });
const tag2 = await manage.save(Tag, { name: "tag2" });
// 用 save() 保存 Post 并关联 tags,中间表才会写入数据
await manage.save(Post, [
{ title: "post1", tags: [tag1] },
{ title: "post2", tags: [tag2] },
]);
})
.catch((error) => console.log(error));增删关联的两种方式
增加关联
- 找到post为3的数据以及他们的关联,如果没关联1,2,就关联1,2 关联了就不添加了
ts
import { AppDataSource } from "./data-source";
import { In } from "typeorm";
import { Tag } from "./entities/Tag";
import { Post } from "./entities/Post";
AppDataSource.initialize()
.then(async () => {
const manage = AppDataSource.manager;
// 查询 id 为 3 的 Post,并加载已关联的 tags
const post = await manage.findOne(Post, {
where: { id: 3 },
relations: { tags: true },
});
// 查询 id 为 1 和 2 的 Tag
const tags = await manage.findBy(Tag, { id: In([1, 2]) });
// 过滤掉已关联的 tag,只保留未关联的
const existingTagIds = post.tags.map((t) => t.id);
const newTags = tags.filter((t) => !existingTagIds.includes(t.id));
if (newTags.length > 0) {
post.tags = [...post.tags, ...newTags];
await manage.save(Post, post);
}
})
.catch((error) => console.log(error));删除关联
ts
import { AppDataSource } from "./data-source";
import { Post } from "./entities/Post";
AppDataSource.initialize()
.then(async () => {
const manage = AppDataSource.manager;
// 查询 id 为 3 的 Post,并加载已关联的 tags
const post = await manage.findOne(Post, {
where: { id: 3 },
relations: { tags: true },
});
// 过滤掉 id 为 1 和 2 的 tag,删除这些关联
post.tags = post.tags.filter((t) => ![1, 2].includes(t.id));
await manage.save(Post, post);
})
.catch((error) => console.log(error));双向关联
ts
import { AppDataSource } from "./data-source";
import { Post } from "./entities/Post";
import { Tag } from "./entities/Tag";
AppDataSource.initialize()
.then(async () => {
const manage = AppDataSource.manager;
// Post -> Tag[]
const post = await manage.find(Post, {
where: { id: 1 },
relations: { tags: true },
});
// Tag -> Post[](反向同样可用)
const tag = await manage.find(Tag, {
where: { id: 3 },
relations: { posts: true },
});
console.log(tag);
console.log(post);
})
.catch((error) => console.log(error));