跳至主要内容

多对多关系

快速摘要

本指南解释了如何在 Prisma 中定义和使用多对多 (m-n) 关系,并提供了关系型数据库和 MongoDB 的示例。

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

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

本页面回答的问题
  • 如何在 Prisma 中建模多对多?
  • 何时使用隐式与显式 m-n?
  • 如何查询 m-n 关系?

关系型数据库

在关系型数据库中,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,
"assignedBy" TEXT NOT NULL,

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,
},
},
},
},
],
},
},
})

以下查询返回所有 Post 记录,其中至少一个(some)类别分配(categories)引用了一个名为 "New category" 的类别

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 关系使得 m-n 关系的 Prisma Client API 更简单(因为在嵌套写入中减少了一层嵌套)。

在下面的示例中,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);
示例

这是一个创建包含索引(PostgreSQL 方言)的三个表的示例 SQL 语句,这些表被 Prisma Introspection 识别为隐式 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")
}

关系表

关系表(有时也称为 JOINlinkpivot 表)连接两个或更多其他表,从而在它们之间创建关系。创建关系表是 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 多对多关系

本节演示了如何使用上述示例模式在 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',
},
},
},
},
})
© . This site is unofficial and not affiliated with Prisma Data, Inc.