TypeORM 多对多关联篇:中间表、JoinTable 与复杂关系的建模

回顾一下前边学过的 一对一一对多

一对一是通过 @OneToOne 和 @JoinColumn 把 Entity 映射成数据库表

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

一对一需要通过 @JoinColumn 来指定外键列,而一对多并不需要,因为外键一定在多的那边。

今天学习多对多。

文章和标签是多对多的关系,一个文章可以有多个标签,一个标签也可以有多篇文章。

多对多是通过中间表来实现的,中间表有两个字段,一个字段是文章的id,另一个字段是标签的id。

在项目中实操一下。

前置工作

  1. 创建 typeorm 项目
bash 复制代码
npx typeorm@latest init --name typeorm_more_to_more_relation_mapping --database mysql
  1. 安装 mysql2 驱动包
bash 复制代码
npm i mysql2
  1. 修改 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

修改

  1. 文章新增标签
  2. 修改文章内容
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))

查询结果:

相关推荐
用户6883362059703 小时前
移动端 Web 性能调优:viewport、dvh 与触控优化解析
前端
AlpsMonaco3 小时前
使用iptables进行网络地址的重定向
后端
前端付豪3 小时前
项目启动:搭建Vue 3工程化项目
前端·javascript·vue.js
Asort3 小时前
JavaScript设计模式(二十)——状态模式 (State):复杂状态管理的优雅解决方案
前端·javascript·设计模式
Penge6663 小时前
Git LFS 上传大文件避坑指南:从忽略报错到成功推送的完整流程
后端
im_AMBER4 小时前
React 07
前端·笔记·学习·react.js·前端框架
Giant1004 小时前
Node.js .env 配置指南:安全管理项目秘钥与多环境适配
前端
SimonKing4 小时前
【开发者必备】Spring Boot 2.7.x:WebMvcConfigurer配置手册来了(四)!
java·后端·程序员
沢田纲吉4 小时前
《LLVM IR 学习手记(七):逻辑运算与位运算的实现与解析》
前端·c++·编译器