多对多关系:隐式与显式
本教程共 54 篇 · 第 12 篇 · 更新于 2026-08-11 · 约 6 分钟阅读
本节目标:理解多对多关系的两种建模方式,学会用 connect/disconnect/set 管理关联,掌握隐式转显式的三步迁移。
一个用户可以收藏多篇文章,一篇文章可以被多个用户收藏。这种「双方都能对应多条记录」的关系,就是多对多关系(many-to-many,简称 m-n)。
关系数据库没有直接的 m-n 概念,需要一张中间表来记录两边的配对。这张表在不同语境里叫法不一:关系表、连接表(join table)、数据透视表。Prisma 提供两种建模方式:隐式与显式。默认情况下,不需要额外信息时用隐式。
隐式多对多:Prisma 帮你管中间表
隐式写法最省事,两侧各放一个列表字段:
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
不需要写 @relation,也不需要声明中间表。Prisma 会在数据库里自动创建一张关系表,表名是 _CategoryToPost:下划线 + 按字母序排列的两个模型名 + To。表里只有两列 A 和 B:A 指向字母序靠前的模型,B 指向另一个。两列组成联合唯一索引,保证同一对记录不重复关联;B 列另带非唯一索引,加速反向查询。
隐式写法有硬性约束:
- 两侧模型都必须有单字段
@id - 不能用复合主键(
@@id),也不能用@unique顶替@id @relation里不能出现 fields、references、onDelete、onUpdate
不满足任何一条,就得改用显式写法。
Note用
prisma db pull内省现有库时,中间表必须符合上述约定(_A 到 B命名、A/B 两列、双列唯一索引),才会被识别成隐式多对多,否则会退化成一张普通模型。
想自定义关系表名,给两侧的关系字段加上同名 @relation 即可:
model Post {
id Int @id @default(autoincrement())
title String
categories Category[] @relation("PostCategories")
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[] @relation("PostCategories")
}
显式多对多:连接表升级为模型
需要记录「由谁关联、什么时候关联的」这类附加信息时,把连接表写成模型:
model Post {
id Int @id @default(autoincrement())
title String
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId Int
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId Int
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
三个要点:连接表用复合主键 @@id 防止重复配对;两条 @relation 都要写全 fields 和 references;附加字段随便加。删除 Post 或 Category 时,引用操作会把连接记录一并清掉。
如果业务上经常按 Category 反查文章列表,可以再给 categoryId 加一个普通索引 @@index([categoryId]),让反向查询也快起来。
查询与关联操作
隐式关系的查询最简洁,分类直接内联:
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { categories: true },
});
显式关系多一层嵌套:
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { categories: { include: { category: true } } },
});
嵌套写入可以一步完成「建文章 + 建分类」,connect 负责挂接已存在的记录:
// 创建文章,同时新建两个分类
await prisma.post.create({
data: {
title: "如何变成蝴蝶",
categories: {
create: [{ name: "魔法" }, { name: "蝴蝶" }],
},
},
});
// 创建文章,关联两个已存在的分类
await prisma.post.create({
data: {
title: "我的文章",
categories: {
connect: [{ id: 9 }, { id: 22 }],
},
},
});
关联已有记录用 connect,断开用 disconnect,整体替换用 set:
// 给文章 1 关联两个已存在的分类
await prisma.post.update({
where: { id: 1 },
data: { categories: { connect: [{ id: 5 }, { id: 7 }] } },
});
// 整组替换:最终只剩 id 1 和 2
await prisma.post.update({
where: { id: 1 },
data: { categories: { set: [{ id: 1 }, { id: 2 }] } },
});
// 清空全部分类
await prisma.post.update({
where: { id: 1 },
data: { categories: { set: [] } },
});
// 一次调用里,既新增又断开
await prisma.post.update({
where: { id: 1 },
data: {
categories: {
connect: [{ id: 5 }],
disconnect: [{ id: 3 }],
},
},
});
set 是幂等的:传什么,最终就是什么。
Tip显式多对多没有隐式多对多那种直接对关系字段 connect 的快捷写法,需要经连接表模型操作。断开关联等于删除连接表记录,直接
prisma.categoriesOnPosts.delete。
按关系过滤与排序
m-n 关系还能当过滤条件用。找出关联了「typescript」或「react」分类的文章:
const posts = await prisma.post.findMany({
where: {
categories: { some: { name: { in: ["typescript", "react"] } } },
},
});
三个操作符:some 表示至少一个满足,every 表示全部满足,none 表示一个都不满足。
排序方面,隐式关系没有固有顺序,include 出来的列表按查询里的 orderBy 排序。显式关系可以在连接表里加 position 字段,实现自定义的稳定顺序。
隐式转显式:三步迁移
业务升级后要给关系补元数据,可以现场改造:
- 保留隐式关系字段,同时新增连接模型,跑一次迁移。此时旧中间表和新连接表并存。
- 写脚本把已有配对复制进连接表:遍历每篇文章,逐个 create 连接记录。
- 删掉两侧的隐式关系字段,再跑一次迁移。Prisma 会顺带删除旧的关系表
_CategoryToPost。
数据迁移脚本(v7 写法):
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client";
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL! }),
});
async function main() {
const posts = await prisma.post.findMany({ include: { categories: true } });
for (const post of posts) {
for (const category of post.categories) {
await prisma.categoriesOnPosts.create({
data: { postId: post.id, categoryId: category.id },
});
}
}
}
main().catch(console.error).finally(() => prisma.$disconnect());
Note数据量大时逐条 create 很慢,建议分批用 createMany 插入。迁移前先备份数据。
MongoDB 的多对多建模完全不同:两侧存 ObjectId 数组,再靠 @relation(fields: [categoryIds], references: [id]) 关联。v7 不支持 MongoDB,这部分留到 v6.19 教程。
参考来源
- Prisma 官方文档:Many-to-many relations
- Prisma 官方文档:Working with many-to-many relations(troubleshooting)
- Mapagam:Working with Many-to-Many Relations