夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
    • Nest 项目如何编写 Dockerfile
    • 提升 Dockerfile 水平的 5 个技巧
    • Docker 是怎么实现的
    • 为什么 Node 应用要用 PM2 来跑?
    • 快速入门 MySQL
    • SQL 查询语句的所有语法和函数
    • 一对一、join 查询、级联方式
    • 一对多、多对多关系的表设计
      • 一对多和多对多的表的设计
      • 一对多的关系,比如一个部门有多个员工。
      • 接下来我们来看多对多。
      • 总结
    • 子查询和 EXISTS
    • SQL 综合练习
    • MySQL 的事务和隔离级别
    • MySQL 的视图、存储过程和函数
    • Node 操作 MySQL 的两种方式
    • 快速掌握 TypeORM
    • TypeORM 一对一的映射和关联 CRUD
    • TypeORM 一对多的映射和关联 CRUD
    • TypeORM 多对多的映射和关联 CRUD
    • 在 Nest 里集成 TypeORM
    • TypeORM保存任意层级的关系
    • 生产环境为什么用TypeORM的migration迁移功能
    • Nest 项目里如何使用 TypeORM 迁移
    • 如何动态读取不同环境的配置?
    • 快速入门 Redis
    • 在 Nest 里操作 Redis
    • 为什么不用 cache-manager 操作 Redis
    • 两种登录状态保存方式:JWT、Session
    • Nest 里实现 Session 和 JWT
    • MySQL + TypeORM + JWT 实现登录注册
    • 基于 ACL 实现权限控制
    • 基于 RBAC 实现权限控制
    • access_token和refresh_token实现无感登录
    • 单token无限续期实现登录无感刷新
    • 使用 passport 做身份认证
    • passport 实现 GitHub 三方账号登录
    • passport 实现 Google 三方账号登录
    • 为什么要使用 Docker Compose ?
    • Docker 容器通信的最简单方式:桥接网络
    • Docker 支持重启策略,是否还需要 PM2
    • 快速掌握 Nginx 的 2 大核心用法
    • 基于 Nginx 实现灰度系统
    • 基于 Redis 实现分布式 session
    • Redis + 高德地图,实现附近的充电宝
    • 用 Swagger 自动生成 api 文档
    • 如何灵活创建 DTO
    • class- validator 的内置装饰器,如何自定义装饰器
    • 序列化 Entity,你不需要 VO 对象
    • 手写序列化 Entity 的拦截器
    • 使用 compodoc 生成文档
    • Node 如何发邮件?
    • 实现基于邮箱验证码的登录
    • 基于 sharp 实现 gif 压缩工具
    • 大文件如何实现流式下载?
    • Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 实现扫二维码登录
    • Nest 的 REPL 模式
    • 实现 Excel 导入导出
    • 如何用代码动态生成 PPT
    • 如何拿到服务器 CPU、内存、磁盘状态
    • Nest 如何实现国际化?
    • 会议室预订系统:需求分析和原型图
    • 会议室预订系统:技术方案和数据库设计
    • 会议室预订系统:用户管理模块--用户注册
    • 会议室预订系统:用户管理模块--配置抽离、登录认证鉴权
    • 会议室预订系统:用户管理模块-- interceptor、修改信息接口
    • 会议室预订系统:用户管理模块--用户列表和分页查询
    • 会议室预订系统:用户管理模块-- swagger 接口文档
    • 会议室预订系统:用户管理模块-- 用户端登录注册页面
    • 会议室预订系统:用户管理模块-- 用户端信息修改页面
    • 会议室预订系统:用户管理模块-- 头像上传
    • 会议室预订系统:用户管理模块-- 管理端用户列表页面
    • 会议室预订系统:用户管理模块-- 管理端信息修改页面
    • 会议室预订系统:会议室管理模块-后端开发
    • 会议室预订系统:会议室管理模块-管理端前端开发
    • 会议室预订系统:会议室管理模块-用户端前端开发
    • 会议室预订系统:预定管理模块-后端开发
    • 会议室预订系统:预定管理模块-管理端前端开发
    • 会议室预订系统:预定管理模块-用户端前端开发
    • 会议室预订系统:统计管理模块-后端开发
    • 会议室预订系统:统计管理模块-前端开发
    • 会议室预订系统:后端项目部署到阿里云
    • 会议室预订系统:前端项目部署到阿里云
    • 会议室预定系统:用 migration 初始化表和数据
    • 会议室预定系统:文件上传 OSS
    • 会议室预定系统:Google 账号登录后端开发
    • 会议室预定系统:Google 账号登录前端开发
    • 会议室预定系统:后端代码优化
    • 会议室预定系统:集成日志框架 winston
    • 会议室预定系统:前端代码优化
    • 会议室预定系统:全部功能测试
    • 会议室预定系统:项目总结
    • Nest 如何创建微服务?
    • Nest 的 Monorepo 和 Library
    • 用 Etcd 实现微服务配置中心和注册中心
    • Nest 集成 Etcd 做注册中心、配置中心
    • 用 Nacos 实现微服务配置中心和注册中心
    • 基于 gRPC 实现跨语言的微服务通信
    • 快速入门 ORM 框架 Prisma
    • Prisma 的全部命令
    • Prisma 的全部 schema 语法
    • Primsa Client 单表 CRUD 的全部 api
    • Prisma Client 多表 CRUD 的全部 api
    • 在 Nest 里集成 Prisma
    • 为什么前端监控系统要用 RabbitMQ?
    • 基于 Redis 实现关注关系
    • 基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 考试系统:需求分析
    • 考试系统:技术方案和数据库设计
    • 考试系统:微服务、Lib 拆分
    • 考试系统;用户注册
    • 考试系统:用户登录、修改密码
    • 考试系统:考试微服务
    • 考试系统:登录、注册页面
    • 考试系统:修改密码、试卷列表页面
    • 考试系统:新增试卷、回收站
    • 考试系统:试卷编辑器
    • 考试系统:试卷回显、预览、保存
    • 考试系统:答卷微服务
    • 考试系统:答题页面
    • 考试系统:自动判卷
    • 考试系统:分析微服务、排行榜页面
    • 考试系统:整体测试
    • 考试系统:项目总结
    • 用 Node.js 手写 WebSocket 协议
    • Nest 开发 WebSocket 服务
    • 基于 Socket.io 的 room 实现群聊
    • 聊天室:需求分析和原型图
    • 聊天室:技术选型和数据库设计
    • 聊天室:用户注册
    • 聊天室:用户登录
    • 聊天室:修改密码、修改信息
    • 聊天室:好友列表、发送好友申请
    • 聊天室:创建聊天室、加入群聊
    • 聊天室:登录、注册页面开发
    • 聊天室:修改密码、信息页面开发
    • 聊天室:头像上传
    • 聊天室:好友∕群聊列表页面
    • 聊天室:添加好友弹窗、通知页面
    • 聊天室:聊天功能后端开发
    • 聊天室:聊天功能前端开发
    • 聊天室:一对一聊天
    • 聊天室:创建群聊、进入群聊
    • 聊天室:发送表情、图片、文件
    • 聊天室:收藏
    • 聊天室:全部功能测试
    • 聊天室:项目总结
    • MongoDB 快速入门
    • 使用 mongoose 操作 MongoDB 数据库
    • GraphQL 快速入门
    • Nest 开发 GraphQL 服务:实现 CRUD
    • GraphQL + Primsa + React 实现 TodoList
    • 如何调试 Nest 源码?
  • 其他

  • 服务端
  • Nest
神说要有光
2025-03-10
目录

一对多、多对多关系的表设计

# 一对多和多对多的表的设计

上节我们学了一对一,这节继续来学习一对多和多对多。

一对多关系在生活中随处可见:

一个作者可以写多篇文章,而每篇文章只属于一个作者。

一个订单有多个商品,而商品只属于一个订单。

一个部门有多个员工,员工只属于一个部门。

多对多的关系也是随处可见:

一篇文章可以有多个标签,一个标签可以多篇文章都有。

一个学生可以选修多门课程,一门课程可以被多个学生选修。

一个用户可以有多个角色,一个角色可能多个用户都有。

那在数据库里如何建模这种关系呢?

我们分别来看一下:

# 一对多的关系,比如一个部门有多个员工。

我们会有一个部门表和一个员工表:

在员工表添加外键 department_id 来表明这种多对一关系:

其实和一对一关系的数据表设计是一样的。

我们添加这两个表。

分别指定 id 是 INT,约束为 primary key、not null、 auto increment 。

name 是 VARCHAR(45),约束为 not null。

点击 apply。

建表 sql 如下:

同样的方式创建 employee 表:

添加 id、name、department_id 这 3 列。

然后添加一个外键约束,department_id 列引用 department 的 id 列。

设置级联删除和更新为 SET NULL。

因为部门没了员工不一定也没了,可能还会分配到别的部门。

点击 apply 就创建成功了。

sql 的最后这段,就是创建了一个外键约束,department_id 引用了 department 表的 id,设置级联删除和更新方式为 SET NULL。

我们往部门表插入几条数据:

INSERT INTO `department` (`id`, `name`)
    VALUES
        (1, '人事部'),
        (2, '财务部'),
        (3, '市场部'),
        (4, '技术部'),
        (5, '销售部'),
        (6, '客服部'),
        (7, '采购部'),
        (8, '行政部'),
        (9, '品控部'),
        (10, '研发部');
1
2
3
4
5
6
7
8
9
10
11
12

查询下:

插入成功了。

再往员工表里插入几条数据:

INSERT INTO `employee` (`id`, `name`, `department_id`)
    VALUES
        (1, '张三', 1),
        (2, '李四', 2),
        (3, '王五', 3),
        (4, '赵六', 4),
        (5, '钱七', 5),
        (6, '孙八', 5),
        (7, '周九', 5),
        (8, '吴十', 8),
        (9, '郑十一', 9),
        (10, '王十二', 10);
1
2
3
4
5
6
7
8
9
10
11
12

查询下:

同样插入成功了。

我们通过 JOIN ON 关联查询下 id 为 5 的部门的所有员工:

select * from department
    join employee on department.id = employee.department_id
    where department.id = 5
1
2
3

可以看到,正确查找出了销售部的 3 个员工:

JOIN ON 默认是 INNER JOIN。

我们再来试试 LEFT JOIN 和 RIGHT JOIN:

select * from department
    left join employee on department.id = employee.department_id
1
2

from 后的是左表,可以看到两个还没有员工的部门也显示在了结果里:

然后是 RIGHT JOIN:

select * from department
    right join employee on department.id = employee.department_id
1
2

因为所有的员工都是有部门的,所以和 inner join 结果一样:

然后把 id 为 5 的部门删掉:

点击 apply。

再查看下员工表,可以看到销售部下的 3 个员工的部门被设置为 null 了:

这就是 set null 的级联删除处理。

一对多是不是还挺简单的?

确实,它和一对一没啥本质的区别。

# 接下来我们来看多对多。

比如文章和标签:

之前一对多关系是通过在多的一方添加外键来引用一的一方的 id。

但是现在是多对多了,每一方都是多的一方。这时候是不是双方都要添加外键呢?

一般我们是这样设计:

文章一个表、标签一个表,这两个表都不保存外键,然后添加一个中间表来保存双方的外键。

这样文章和标签的关联关系就都被保存到了这个中间表里。

我们试一下:

这次我们就直接通过 sql 建表了:

CREATE TABLE `article` (
 `id` INT NOT NULL AUTO_INCREMENT,
 `title` VARCHAR(50) NOT NULL,
 `content` TEXT NOT NULL,
 PRIMARY KEY (`id`)
) CHARSET=utf8mb4;
1
2
3
4
5
6

这里的 TEXT 是长文本类型,可以存储 65535 长度的字符串。

执行这个建表 sql:

查询下:

插入几条数据:

INSERT INTO `article` (`title`, `content`)
    VALUES
            ('文章1', '这是文章1的内容。'),
            ('文章2', '这是文章2的内容。'),
            ('文章3', '这是文章3的内容。'),
            ('文章4', '这是文章4的内容。'),
            ('文章5', '这是文章5的内容。');
1
2
3
4
5
6
7

再查询下:

然后创建标签表:

CREATE TABLE `tag` (
 `id` INT NOT NULL AUTO_INCREMENT,
 `name` VARCHAR(50) NOT NULL,
 PRIMARY KEY (`id`)
);
1
2
3
4
5

插入几条数据:

INSERT INTO `tag` (`name`)
    VALUES
            ('标签1'),
            ('标签2'),
            ('标签3'),
            ('标签4'),
            ('标签5');
1
2
3
4
5
6
7

然后查询下:

可以看到,建表和插入数据都成功了。

然后创建中间表:

中间表还是通过可视化的方式创建吧:

点击创建表,添加 article_id、tag_id 这俩列,设置为 NOT NULL。

注意,这里同时指定这两列为 primary key,也就是复合主键。

添加 article_id 和 tag_id 的外键引用:

中间表的级联方式要设置为 CASCADE,这个是固定的。

因为它就是用来保存关系的,如果关联的记录都没了,这个关系也就没存在的意义了。

点击 apply,可以看到生成的 sql:

primary key (article_id, tag_id) 是指定复合主键。

后面分别是添加两个外键约束。

建表 sql 能看懂即可,不需要手写。

然后插入几条数据:

INSERT INTO `article_tag` (`article_id`, `tag_id`)
    VALUES
    (1,1), (1,2), (1,3),
    (2,2), (2,3), (2,4),
    (3,3), (3,4), (3,5),
    (4,4), (4,5), (4,1),
    (5,5), (5,1), (5,2);
1
2
3
4
5
6
7

点击左上角按钮,新建一条 sql:

输入 sql 后点击执行。

然后就可以看到插入的数据了:

那现在有了 article、tag、article_tag 3 个表了,怎么关联查询呢?

JOIN 3 个表呀!

SELECT * FROM article a
    JOIN article_tag at ON a.id = at.article_id
    JOIN tag t ON t.id = at.tag_id
    WHERE a.id = 1
1
2
3
4

这样查询出的就是 id 为 1 的 article 的所有标签:

当然,一般我们会指定返回的列:

SELECT t.name AS 标签名, a.title AS 文章标题
    FROM article a
    JOIN article_tag at ON a.id = at.article_id
    JOIN tag t ON t.id = at.tag_id
    WHERE a.id = 1
1
2
3
4
5

此外,我们把文章 1 删除试试:

选中这条 sql 执行:

delete from article where id = 1;
1

然后再执行下面的查询:

可以看到关系也被级联删除了,这就是 CASCADE 的作用。

当然,删除的只是关系,并不影响 id=1 的标签:

这就是多对多数据的表设计、关联查询和级联方式。

# 总结

现实生活中有很多的一对多、多对多关系。

我们创建了部门、员工表,并在员工表添加了引用部门 id 的外键 department_id 来保存这种一堆多关系。

并且设置了级联方式为 set null。

创建了文章表、标签表、文章标签表来保存多堆多关系,多对多不需要在双方保存彼此的外键,只要在中间表里维护这种关系即可。

中间表的外键级联方式一定为 CASCADE,因为数据没了关系就没必要还留着了。

此外,多对多的 join 需要连接 3 个表来查询。

一对多、多对多是非常常见的表之间的关系,要好好掌握它们的外键设置、关联查询、级联方式 。

编辑 (opens new window)
上次更新: 2025/6/5 17:42:44
一对一、join 查询、级联方式
子查询和 EXISTS

← 一对一、join 查询、级联方式 子查询和 EXISTS→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
IoC 解决了什么痛点问题?
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式