跳到主要内容

多对多关系

多对多 (m-n) 关系是指关系中一侧的零个或多个记录可以连接到另一侧的零个或多个记录。

Prisma schema 语法和底层数据库中的实现因关系型数据库MongoDB 而异。

关系型数据库

在关系型数据库中,m-n 关系通常通过关系表建模。m-n 关系在 Prisma schema 中可以是显式的或隐式的。如果您不需要在关系表本身存储任何额外的元数据,我们建议使用隐式 m-n 关系。如果需要,您以后随时可以迁移到显式 m-n 关系。

显式多对多关系

在显式 m-n 关系中,关系表在 Prisma schema 中表示为一个模型,可以在查询中使用。显式 m-n 关系定义了三个模型

  • 两个具有 m-n 关系的模型,例如 CategoryPost
  • 一个代表关系表的模型,例如底层数据库中的 CategoriesOnPosts(有时也称为 JOIN 表、link 表或 pivot 表)。关系表模型的字段都是带有相应关系标量字段(postIdcategoryId)的注解关系字段(postcategory)。

关系表 CategoriesOnPosts 连接相关的 PostCategory 记录。在此示例中,代表关系表的模型还定义了额外的字段来描述 Post/Category 关系 - 谁分配了类别(assignedBy),以及何时分配了类别(assignedAt

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])
postId Int // relation scalar field (used in the `@relation` attribute above)
category Category @relation(fields: [categoryId], references: [id])
categoryId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String

@@id([postId, categoryId])
}

底层 SQL 如下所示:

CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,

CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,

CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);


-- Relation table + indexes --

CREATE TABLE "CategoriesOnPosts" (
"postId" INTEGER NOT NULL,
"categoryId" INTEGER NOT NULL,
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "CategoriesOnPosts_pkey" PRIMARY KEY ("postId","categoryId")
);

ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

请注意,与一对多关系相同的规则适用(因为 PostCategoriesOnPostsCategoryCategoriesOnPosts 实际上都是一对多关系),这意味着关系的一侧需要使用 @relation 属性进行注解。

当您不需要向关系附加额外信息时,可以将 m-n 关系建模为隐式 m-n 关系。如果您不使用 Prisma Migrate,而是通过内省获取数据模型,您仍然可以通过遵循 Prisma ORM 的关系表约定来利用隐式 m-n 关系。

查询显式多对多关系

以下部分演示如何查询显式 m-n 关系。您可以直接查询关系模型(prisma.categoriesOnPosts(...)),或使用嵌套查询从 Post -> CategoriesOnPosts -> Category 或反向查询。

以下查询执行三件事:

  1. 创建一个 Post
  2. 在关系表 CategoriesOnPosts 中创建一条新记录
  3. 创建一个与新创建的 Post 记录关联的新 Category
const createCategory = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
create: {
name: 'New category',
},
},
},
],
},
},
})

以下查询:

  • 创建一个新 Post
  • 在关系表 CategoriesOnPosts 中创建一条新记录
  • 将类别分配连接到现有类别(ID 为 922
const assignCategories = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connect: {
id: 9,
},
},
},
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connect: {
id: 22,
},
},
},
],
},
},
})

有时您可能不知道 Category 记录是否存在。如果 Category 记录存在,您想将新的 Post 记录连接到该类别。如果 Category 记录不存在,您想先创建该记录,然后将其连接到新的 Post 记录。以下查询:

  1. 创建一个新 Post
  2. 在关系表 CategoriesOnPosts 中创建一条新记录
  3. 将类别分配连接到现有类别(ID 为 9),如果不存在则先创建一个新类别
const assignCategories = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connectOrCreate: {
where: {
id: 9,
},
create: {
name: 'New Category',
id: 9,
},
},
},
},
],
},
},
})

以下查询返回所有至少有一个 (some) 类别分配 (categories) 指向名为 "New category" 的类别的 Post 记录。

const getPosts = await prisma.post.findMany({
where: {
categories: {
some: {
category: {
name: 'New Category',
},
},
},
},
})

以下查询返回所有至少有一个 (some) 相关 Post 记录标题包含单词 "Cool stuff" 且该类别由 Bob 分配的类别。

const getAssignments = await prisma.category.findMany({
where: {
posts: {
some: {
assignedBy: 'Bob',
post: {
title: {
contains: 'Cool stuff',
},
},
},
},
},
})

以下查询获取所有由 "Bob" 分配给 5 个帖子之一的类别分配 (CategoriesOnPosts) 记录。

const getAssignments = await prisma.categoriesOnPosts.findMany({
where: {
assignedBy: 'Bob',
post: {
id: {
in: [9, 4, 10, 12, 22],
},
},
},
})

隐式多对多关系

隐式 m-n 关系将关系字段定义为关系两端的列表。虽然关系表存在于底层数据库中,但它由 Prisma ORM 管理,并且不会在 Prisma schema 中显现。隐式关系表遵循特定约定

隐式 m-n 关系使得 Prisma Client API 在处理 m-n 关系时更加简单(因为在嵌套写入中减少了一层嵌套)。

在下面的示例中,PostCategory 之间存在一个隐式 m-n 关系:

model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}

model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}

查询隐式多对多关系

以下部分演示如何查询隐式 m-n 关系。相比显式 m-n 查询,这些查询需要更少的嵌套。

以下查询创建一个 Post 记录和多个 Category 记录:

const createPostAndCategory = await prisma.post.create({
data: {
title: 'How to become a butterfly',
categories: {
create: [{ name: 'Magic' }, { name: 'Butterflies' }],
},
},
})

以下查询创建一个 Category 记录和多个 Post 记录:

const createCategoryAndPosts = await prisma.category.create({
data: {
name: 'Stories',
posts: {
create: [
{ title: 'That one time with the stuff' },
{ title: 'The story of planet Earth' },
],
},
},
})

以下查询返回所有 Post 记录及其分配的类别列表:

const getPostsAndCategories = await prisma.post.findMany({
include: {
categories: true,
},
})

定义隐式 m-n 关系的规则

隐式 m-n 关系

  • 使用特定的关系表约定

  • 不需要 @relation 属性,除非您需要使用名称来区分关系,例如 @relation("MyRelation")@relation(name: "MyRelation")

  • 如果您使用了 @relation 属性,则不能使用 referencesfieldsonUpdateonDelete 参数。这是因为这些参数对于隐式 m-n 关系采用固定值,无法更改。

  • 要求两个模型都具有单个 @id。请注意:

    • 您不能使用多字段 ID
    • 您不能使用 @unique 代替 @id
    信息

    要使用其中任一特性,您必须改用显式 m-n

隐式 m-n 关系中关系表的约定

如果您通过内省获取数据模型,您仍然可以通过遵循 Prisma ORM 的关系表约定来使用隐式 m-n 关系。以下示例假设您想创建一个关系表,以便为名为 PostCategory 的两个模型获取隐式 m-n 关系。

关系表

如果您希望内省将某个关系表识别为隐式 m-n 关系,则其名称必须遵循以下精确结构:

  • 必须以下划线 _ 开头
  • 然后是按字母顺序排列的第一个模型的名称(此处为 Category
  • 然后是关系(此处为 To
  • 然后是按字母顺序排列的第二个模型的名称(此处为 Post

在此示例中,正确的表名称是 _CategoryToPost

当您自己在 Prisma schema 文件中创建隐式 m-n 关系时,您可以配置该关系以使用不同的名称。这将改变数据库中关系表的名称。例如,对于名为 "MyRelation" 的关系,对应的表将命名为 _MyRelation

多模式

如果您的隐式多对多关系跨越多个数据库模式(使用multiSchema 预览特性),则关系表(名称如上文定义,在本例中为 _CategoryToPost)必须与按字母顺序排列的第一个模型(此处为 Category)位于同一个数据库模式中。

隐式 m-n 关系的关系表必须恰好有两列:

  • 一个指向 Category 的外键列,命名为 A
  • 一个指向 Post 的外键列,命名为 B

列必须命名为 AB,其中 A 指向按字母顺序排在前面的模型,B 指向按字母顺序排在后面的模型。

索引

此外还必须有:

  • 在两个外键列上定义的唯一索引

    CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A" int4_ops,"B" int4_ops);
  • 在 B 列上定义的非唯一索引

    CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B" int4_ops);
示例

这是一个示例 SQL 语句,它将创建三个表(包括索引,使用 PostgreSQL 方言),Prisma 内省会将其识别为隐式 m-n 关系:

CREATE TABLE "_CategoryToPost" (
"A" integer NOT NULL REFERENCES "Category"(id) ,
"B" integer NOT NULL REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A" int4_ops,"B" int4_ops);
CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B" int4_ops);

CREATE TABLE "Category" (
id integer SERIAL PRIMARY KEY
);

CREATE TABLE "Post" (
id integer SERIAL PRIMARY KEY
);

您还可以通过使用不同的关系名称在两个表之间定义多个多对多关系。此示例展示了 Prisma 内省在此类情况下的工作方式:

CREATE TABLE IF NOT EXISTS "User" (
"id" SERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS "Video" (
"id" SERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS "_UserLikedVideos" (
"A" SERIAL NOT NULL,
"B" SERIAL NOT NULL,
CONSTRAINT "_UserLikedVideos_A_fkey" FOREIGN KEY ("A") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_UserLikedVideos_B_fkey" FOREIGN KEY ("B") REFERENCES "Video" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS "_UserDislikedVideos" (
"A" SERIAL NOT NULL,
"B" SERIAL NOT NULL,
CONSTRAINT "_UserDislikedVideos_A_fkey" FOREIGN KEY ("A") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_UserDislikedVideos_B_fkey" FOREIGN KEY ("B") REFERENCES "Video" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "_UserLikedVideos_AB_unique" ON "_UserLikedVideos"("A", "B");
CREATE INDEX "_UserLikedVideos_B_index" ON "_UserLikedVideos"("B");
CREATE UNIQUE INDEX "_UserDislikedVideos_AB_unique" ON "_UserDislikedVideos"("A", "B");
CREATE INDEX "_UserDislikedVideos_B_index" ON "_UserDislikedVideos"("B");

如果您对此数据库运行 prisma db pull,Prisma CLI 将通过内省生成以下 schema:

model User {
id Int @id @default(autoincrement())
Video_UserDislikedVideos Video[] @relation("UserDislikedVideos")
Video_UserLikedVideos Video[] @relation("UserLikedVideos")
}

model Video {
id Int @id @default(autoincrement())
User_UserDislikedVideos User[] @relation("UserDislikedVideos")
User_UserLikedVideos User[] @relation("UserLikedVideos")
}

在隐式多对多关系中配置关系表的名称

使用 Prisma Migrate 时,您可以使用 @relation 属性配置由 Prisma ORM 管理的关系表的名称。例如,如果您希望关系表命名为 _MyRelationTable 而不是默认名称 _CategoryToPost,可以按如下方式指定:

model Post {
id Int @id @default(autoincrement())
categories Category[] @relation("MyRelationTable")
}

model Category {
id Int @id @default(autoincrement())
posts Post[] @relation("MyRelationTable")
}

关系表

关系表(有时也称为 JOIN 表、link 表或 pivot 表)连接两个或更多其他表,从而在它们之间建立关系。创建关系表是 SQL 中常见的数据建模实践,用于表示不同实体之间的关系。本质上,这意味着“一个 m-n 关系在数据库中被建模为两个 1-n 关系”。

我们建议使用隐式 m-n 关系,其中 Prisma ORM 会在底层数据库中自动生成关系表。显式 m-n 关系应在您需要在关系中存储额外数据(例如关系创建日期)时使用。

MongoDB

在 MongoDB 中,m-n 关系由以下元素表示:

  • 两边的关系字段,每个字段都有一个 @relation 属性,并带有强制性的 fieldsreferences 参数
  • 两边的引用 ID 的标量列表,其类型与另一侧的 ID 字段类型匹配

以下示例演示了帖子和类别之间的 m-n 关系:

model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
categoryIDs String[] @db.ObjectId
categories Category[] @relation(fields: [categoryIDs], references: [id])
}

model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
postIDs String[] @db.ObjectId
posts Post[] @relation(fields: [postIDs], references: [id])
}

Prisma ORM 根据以下规则验证 MongoDB 中的 m-n 关系:

  • 关系两边的字段必须是列表类型(在上面的示例中,categories 的类型是 Category[]posts 的类型是 Post[]
  • @relation 属性必须在关系两边定义 fieldsreferences 参数
  • fields 参数必须只定义一个标量字段,并且该字段必须是列表类型
  • references 参数必须只定义一个标量字段。此标量字段必须存在于引用的模型上,并且必须与 fields 参数中的标量字段类型相同,但为单数类型(非列表类型)
  • references 指向的标量字段必须具有 @id 属性
  • @relation 中不允许使用参照动作

MongoDB 不支持在关系型数据库中使用的隐式 m-n 关系。

查询 MongoDB 多对多关系

本节演示如何使用上面的示例 schema 查询 MongoDB 中的 m-n 关系。

以下查询查找具有特定匹配类别 ID 的帖子:

const newId1 = new ObjectId()
const newId2 = new ObjectId()

const posts = await prisma.post.findMany({
where: {
categoryIDs: {
hasSome: [newId1.toHexString(), newId2.toHexString()],
},
},
})

以下查询查找类别名称包含字符串 'Servers' 的帖子:

const posts = await prisma.post.findMany({
where: {
categories: {
some: {
name: {
contains: 'Servers',
},
},
},
},
})