首页 / Prisma ORM 入门教程 / 多对多关系:隐式与显式

Prisma ORM 入门教程

多对多关系:隐式与显式

本教程共 54 篇 · 第 12 篇 · 更新于 2026-08-11 · 约 6 分钟阅读

Prisma多对多隐式关系显式关系连接表connectschema建模

本节目标:理解多对多关系的两种建模方式,学会用 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 字段,实现自定义的稳定顺序。

隐式转显式:三步迁移

业务升级后要给关系补元数据,可以现场改造:

  1. 保留隐式关系字段,同时新增连接模型,跑一次迁移。此时旧中间表和新连接表并存。
  2. 写脚本把已有配对复制进连接表:遍历每篇文章,逐个 create 连接记录。
  3. 删掉两侧的隐式关系字段,再跑一次迁移。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