跳到主要内容

多对多关系

多对多 (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. 创建一个新的 Category,它与新创建的 Post 记录相关联
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 关系使 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);
示例

这是一个示例 SQL 语句,它将创建三个表(包括 Prisma Introspection 作为隐式 m-n 关系提取的索引)(使用 PostgreSQL 方言)

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中不允许使用任何引用操作

在 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',
},
},
},
},
})