Insights·2026-09-30

多对多(N:M)关系怎么处理 — 中间表、连接与 Mermaid ERD

多对多(N:M)关系不是把列表塞进一个格子,而是建一张中间表,每行记下两张表的一对 id(指向每一行的唯一值),把它拆成两个 1:N(一边一个、另一边多个的关系)。手机请柬服务增设副管理员、一份请柬由多名管理员共同负责的那一刻,就是例子。中间表里还可以记下 owner、sub 这样的角色。连接(join)是用 id 从别的表把自己表里没有的信息拉过来贴上。ERD 是画出表与表之间关系的设计图,请 AI 用 Mermaid(把文字写成的代码变成图的工具)的 erDiagram 格式画出来,就能用眼睛确认。

다대다(N:M)는 중간 테이블로 1:N 두 개로 푼다 — 글의 요약 도식

有了副管理员,1:N 就变成 N:M

到上一篇为止,手机请柬服务里的关系都是 1:N。一位管理员创建多份请柬,一份请柬只对应创建它的那一位管理员。所以请柬表上有一个 admin_id 列就够了。这个格子里放的是指向管理员表中那位管理员所在行的 id。

假设服务做起来了,招了好几位管理员,请柬也多了起来。如果只允许创建请柬的管理员修改,那么等联系上这位管理员再改好,要花太长时间。于是需要能更快响应的副管理员。

现在一份请柬由多位管理员负责,一位管理员也照样负责多份请柬。两边都是「多」的这种关系叫多对多,写作 N:M。在 1:N 里「多」只挂在一边,在 N:M 里两边都挂着。

到这里,本系列看过的关系一共三种:作为基础的 1:N、偶尔使用的可选 1:1,以及这次的 N:M。

不往一个格子里放列表 — 用中间表来解

最先想到的办法,是在 admin_id 格子里以列表形式放多位管理员,比如在请柬 1 的格子里一次写下管理员 1、2、3。意思上说得通,但表不是这么用的。关系型数据库的设计以「一个格子放一个值」为基础。

取而代之,把关系一行一条地记下来:请柬 1 与管理员 1、请柬 1 与管理员 2、请柬 1 与管理员 3、请柬 2 与管理员 4。为此另建一张只记关系的表。在课程的例子里它叫「请柬管理」,格子是自己的 id、请柬 id 和管理员 id。

这样一个 N:M 就变成了两个 1:N。一份请柬下挂着多行请柬管理,一位管理员下也挂着多行请柬管理。只是把上一篇的 1:N 用了两次,没有新规则要学。

这张中间表有专门的叫法。课程里称它为连接对象(junction object),意思是为了说明两张表之间的关系而建的对象。连接表、关联表这些叫法指的也是同一个东西。

一行记一条关系的中间表
invitations       admins            invitation_admins
───────────       ──────────        ──────────────────────────────
id  groom_name    id  name          id  invitation_id  admin_id
1   敏秀          1   智恩          1   1              1
2   俊浩          2   书俊          2   1              2
                  3   夏琳          3   1              3
                  4   道允          4   2              4

invitations 1 : N invitation_admins
admins      1 : N invitation_admins

中间表里可以记下角色

把关系单独拿出来还有额外的好处:可以给中间表加更多的列。这等于有了一个记录关系本身信息的位置。

课程的例子是 role(角色)列。对请柬 1 来说,管理员 1 是最初创建的人,所以是 owner;其余的人不是主人,但能处理事务,所以记为 sub。同一位管理员在不同请柬上的角色可能不同,所以这个值既不该放在管理员表,也不该放在请柬表,而应放在两者之间的那一行里。

所以遇到 N:M,主要的解法就是建一张中间表,把两边分别用 1:N 连上。原本只有管理员和请柬的简单 ERD,每加一个功能就会这样扩展开来。

向 AI 提这个结构时,用文字把形状定下来就行。像下面的请求那样先写明中间表的名字和格子,就不太会得到把管理员列表塞进一个格子的答案。把已有请柬的 admin_id 值搬到新表,属于迁移(migration),也就是把现有数据挪到新结构的变更,需要单独确认。这部分在第 4 篇讲。

invitation_idadmin_idrole
11owner
12sub
13sub
24owner
给 AI 的请求
我想让一份请柬可以由多位管理员管理。
不要在请柬表的 admin_id 格子里放管理员列表,
请建中间表 invitation_admins(id, invitation_id, admin_id, role),
把 invitations 和 admins 分别用 1:N 连上。
role 里只允许 owner 或 sub。
动手之前,先说明现有的请柬数据要怎么迁移。

连接 — 用 id 拉来自己表里没有的信息

这次假设要做请柬列表页面。除了新郎、新娘这些请柬信息,还想一眼看到这份请柬用的是哪个模板,以及该模板的 URL 和缩略图(小尺寸的预览图)。

可是请柬表里没有模板 URL。上一篇把模板拆成了单独的表,所以请柬那一行只有 template_id,URL 和缩略图在模板表的行里。

于是拿着请柬行里的 template_id,到模板表里找 id 相同的那一行,把那一行的 URL 取过来,以 template_url 的名字贴在请柬行旁边。自己的表里没有、但父表里有的信息,用 id 拉过来贴上,这就是连接(join)。

把列表页交给 AI 时,加一句「模板 URL 和缩略图从模板表连接过来」就行。比起把同样的值再复制一份到请柬表,用一个 id 拉取的方式在修改模板时不会出现数值对不上。

按 template_id 找到模板行并贴上
invitations                                templates
───────────────────────────────────────    ────────────────────────────────
id  groom_name  bride_name  template_id    id    url              thumbnail
1   敏秀        书妍        t-03           t-03  /t/spring        spring.png

列表页上显示的一行(连接结果)
────────────────────────────────────────────────────────────
id  groom_name  bride_name  template_url  template_thumbnail
1   敏秀        书妍        /t/spring     spring.png

用 Mermaid 请 AI 画出 ERD

关系多到这个程度,光靠文字就很难跟上了。这时用的是 Mermaid。这个名字在英语里是美人鱼的意思,它是把用文字写成的代码变成图的工具。

和 AI 一起工作,会大量使用 Markdown 文档。Markdown 是用 #、- 这类符号写标题和列表的文档格式,像 README.md 这样名字以 .md 结尾的文件就是。在这种文档里用三个反引号(`)开一个代码块,写上 mermaid,再在下一行写 erDiagram 以及表和关系,支持 Mermaid 的界面就会自动画出 ERD。

不需要自己写代码。课程推荐的做法是对 AI 说「用 Mermaid 画出 ERD」。下面就是把本篇出现的四张表这样写出来的代码。||--o{ 这样的符号就是第 2 篇讲的鸦爪表示法;PK 是指向该行的唯一 id(主键),FK 是指向别的表 id 的格子(外键)。

课程说的要领是:看着画出来的图,亲手把关系一条条过一遍。在图上确认请柬和管理员之间有没有中间表,列表页需要的信息是从哪张表连接过来的。下一篇看看把这样画好的结构交给 AI 时会出事的四种情况,以及如何从「列表很慢」「注册成功了积分却没到账」这样的症状认出它们。

给 AI 的请求
把这个项目现在的数据库结构画成 Mermaid erDiagram。
每张表都写上列名、数据类型和 PK·FK 标记,
关系用鸦爪表示法画出来,让人看出是 1:N 还是 N:M。
如果有 N:M,也说明是用哪张中间表解开的。
请柬服务 ERD(Mermaid)
```mermaid
erDiagram
    admins ||--o{ invitation_admins : manages
    invitations ||--|{ invitation_admins : "managed by"
    templates ||--o{ invitations : "used by"

    admins {
        uuid id PK
        varchar name
    }
    invitations {
        uuid id PK
        varchar groom_name
        varchar bride_name
        uuid template_id FK
    }
    invitation_admins {
        uuid id PK
        uuid invitation_id FK
        uuid admin_id FK
        varchar role "owner or sub"
    }
    templates {
        uuid id PK
        varchar url
        varchar thumbnail
    }
```