跳到主要内容

多对多关系

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

Prisma 模式语法和底层数据库中的实现方式在关系型数据库MongoDB之间有所不同。

关系型数据库

在关系型数据库中,m-n-关系通常通过关系表进行建模。m-n-关系可以在 Prisma 模式中是显式的,也可以是隐式的。我们建议在不需要在关系表本身存储任何其他元数据的情况下使用隐式 m-n-关系。如果需要,你始终可以稍后迁移到显式 m-n-关系。

显式多对多关系

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

  • 两个具有 m-n 关系的模型,例如 CategoryPost
  • 一个代表关系表的模型,例如底层数据库中的 CategoriesOnPosts(有时也称为JOINlinkpivot 表)。关系表模型的字段都是带有一个相应关系标量字段(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;

请注意,与1-n 关系相同的规则适用(因为 PostCategoriesOnPostsCategoryCategoriesOnPosts 实际上都是 1-n 关系),这意味着关系的一侧需要用 @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 模式中体现。隐式关系表遵循特定约定

隐式 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 模式文件中自己创建隐式 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 将通过自省生成以下模式。

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链接枢纽 表)连接两个或多个其他表,从而在它们之间创建 关系。创建关系表是 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 不允许使用任何 引用操作

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

查询 MongoDB 多对多关系

本节展示如何使用上面的示例模式在 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',
},
},
},
},
})