回顾一下前边学过的 一对一 和 一对多。
一对一是通过 @OneToOne 和 @JoinColumn 把 Entity 映射成数据库表。

一对多是通过 @OneToMany 和 @ManyToOne 把 Entity 映射成数据库表。

一对一需要通过 @JoinColumn 来指定外键列,而一对多并不需要,因为外键一定在多的那边。
今天学习多对多。
文章和标签是多对多的关系,一个文章可以有多个标签,一个标签也可以有多篇文章。
多对多是通过中间表来实现的,中间表有两个字段,一个字段是文章的id,另一个字段是标签的id。

在项目中实操一下。
前置工作
- 创建 typeorm 项目
bash
npx typeorm@latest init --name typeorm_more_to_more_relation_mapping --database mysql
- 安装 mysql2 驱动包
bash
npm i mysql2
- 修改 data-source.ts 配置
ts
import "reflect-metadata"
import {DataSource} from "typeorm"
import {User} from "./entity/User"
export const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "typeorm_test",
synchronize: true,
logging: true,
entities: [User],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password'
}
})
具体参数配置解释可见之前的文章: TypeORM 基础篇:项目初始化与增删改查全流程。
创建 Article 和员工 Tag 两个实体
ts
import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 255,
comment: '文章标题'
})
title: string
@Column({
type: 'text',
comment: '文章内容'
})
content: string
}
ts
import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 100
})
name: string
}
把这两个实体放到 data-source 的 entities 属性中:

删除 User 实体以及 index.ts 中相关代码。
ts
import { AppDataSource } from "./data-source"
AppDataSource.initialize().then(async () => {
console.log('...')
}).catch(error => console.log(error))
添加多对多映射
在 Entity 中通过 @ManyToMany 实现多对多映射, 在这里就是一篇文章可以有多个标签。

执行 npm run start 可以看到以下内容:
- 三条建表 sql,分别对应 article、tag 和 中间表 article_tags_tag。
- article_tags_tag 表还有两个外键分别引用着 article 和 tag 表。
- 级联关系也都是 CASCADE,也就是说删除这两个标的记录,对应在中间表中的记录也会跟着被删掉。

插入数据
创建两篇文章、三个标签,建立它们的关系之后,先保存 tag、再保存 article。
ts
import { AppDataSource } from "./data-source"
import {Article} from "./entity/Article";
import { Tag } from "./entity/Tag";
AppDataSource.initialize().then(async () => {
const a1 = new Article()
a1.title = '文章1'
a1.content = '内容1'
const a2 = new Article()
a2.title = '文章2'
a2.content = '内容2'
const t1 = new Tag()
t1.name = '标签1'
const t2 = new Tag()
t2.name = '标签2'
const t3 = new Tag()
t3.name = '标签3'
a1.tags = [t1, t2, t3]
a2.tags = [t1, t2]
await AppDataSource.manager.save([t1, t2, t3])
await AppDataSource.manager.save([a1, a2])
}).catch(error => console.log(error))
执行 npm run start 可以看到数据插入 sql

数据库中也可以看到对应数据

查询
ts
import { AppDataSource } from "./data-source"
import {Article} from "./entity/Article";
import { Tag } from "./entity/Tag";
AppDataSource.initialize().then(async () => {
const article = await AppDataSource.manager.find(Article, {
relations: {
tags: true
}
})
console.log('article: ', article)
const tags = article.map(item => item.tags)
console.log('tags: ', tags)
}).catch(error => console.log(error))
执行 npm run start

修改
- 文章新增标签
- 修改文章内容
ts
import { AppDataSource } from "./data-source"
import {Article} from "./entity/Article";
import { Tag } from "./entity/Tag";
AppDataSource.initialize().then(async () => {
// 修改
const article = await AppDataSource.manager.findOne(Article, {
where: {
id: 7
},
relations: {
tags: true
}
})
article.content = '修改后的文章内容-----三十'
const t5 = new Tag()
t5.name = '标签5'
await AppDataSource.manager.save(t5)
article.tags = [...article.tags, t5]
await AppDataSource.manager.save(article)
const articles = await AppDataSource.manager.find(Article, {
relations: {
tags: true
}
})
console.log('articles: ', articles)
const tags = articles.map(item => item.tags)
console.log('tags: ', tags)
}).catch(error => console.log(error))
执行 npm run start 后的 sql 记录:

查询结果:

删除
在创建中间表的时候设置了外键的级联关系是 CASCADE,这样在删除 article 或 tag 表时,它都会跟着删除关联记录。
ts
await AppDataSource.manager.delete(Article, 7)
await AppDataSource.manager.delete(Tag, 15)
console.log('删除后查询: ', await AppDataSource.manager.find(Article, {
relations: {
tags: true
}
}))
console.log('删除后查询: ', await AppDataSource.manager.find(Tag))
提示
问: 如果当前 Entity 对应的表是包含外键的,那它自然知道怎么找到关联的 Entity。但是如果当前 Entity 是不包含外键的一方,怎么找到对方呢?
答: 需要手动指定通过哪个外键列来找到当前 Entity。
举例:
一对一的 user 方,不维护外键,所以需要第二个参数来指定通过哪个外键找到 user。

一对多的 department 那方,不维护外键,所以需要第二个参数来指定通过哪个外键找到 department。

多对多时,双方都不维护外键,所以都需要第二个参数来指定外键列在哪里,怎么找到当前的 Entity。


多对多对应查询:
ts
import { AppDataSource } from "./data-source"
import {Article} from "./entity/Article";
import { Tag } from "./entity/Tag";
AppDataSource.initialize().then(async () => {
const tags = await AppDataSource.manager.find(Tag, {
relations: {
articles: true
}
})
console.log('tags: ', tags)
}).catch(error => console.log(error))
查询结果:
