多对多关系
多对多 (m-n) 关系指的是关系的一方零个或多个记录可以连接到关系的另一方零个或多个记录。
Prisma schema 语法和底层数据库中的实现方式在关系型数据库和 MongoDB 之间有所不同。
关系型数据库
在关系型数据库中,m-n 关系通常通过关系表建模。m-n 关系在 Prisma schema 中可以是显式的或隐式的。如果您不需要在关系表本身中存储任何额外的元数据,我们建议使用隐式 m-n 关系。如果需要,您可以随时迁移到显式 m-n 关系。
显式多对多关系
在显式 m-n 关系中,关系表在 Prisma schema 中表示为一个模型,并且可以在查询中使用。显式 m-n 关系定义了三个模型
- 两个具有 m-n 关系的模型,例如
Category
和Post
。 - 一个表示关系表的模型,例如底层数据库中的
CategoriesOnPosts
(有时也称为JOIN、link 或 pivot 表)。关系表模型的字段既是带注释的关系字段(post
和category
),也是对应的关系标量字段(postId
和categoryId
)。
关系表 CategoriesOnPosts
连接相关的 Post
和 Category
记录。在本例中,表示关系表的模型还定义了描述 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 关系相同的规则适用(因为 Post
↔ CategoriesOnPosts
和 Category
↔ CategoriesOnPosts
实际上都是 1-n 关系),这意味着关系的一方需要使用 @relation
属性进行注释。
当您不需要将额外的信息附加到关系时,您可以将 m-n 关系建模为隐式 m-n 关系。如果您不使用 Prisma Migrate,而是从内省获取数据模型,您仍然可以通过遵循 Prisma ORM 的 关系表约定来利用隐式 m-n 关系。
查询显式多对多关系
以下部分演示了如何查询显式 m-n 关系。您可以直接查询关系模型 (prisma.categoriesOnPosts(...)
),或者使用嵌套查询从 Post
-> CategoriesOnPosts
-> Category
或反之亦然。
以下查询执行三件事
- 创建
Post
- 在关系表
CategoriesOnPosts
中创建新记录 - 创建一个与新创建的
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 为
9
和22
)
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
记录。以下查询
- 创建一个新的
Post
- 在关系表
CategoriesOnPosts
中创建新记录 - 将类别分配连接到现有类别(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 schema 中显现。隐式关系表遵循特定约定。
隐式 m-n 关系使 Prisma Client API 对于 m-n 关系来说更简单一些(因为您在 嵌套写入 中减少了一层嵌套)。
在下面的示例中,Post
和 Category
之间存在一个隐式 m-n 关系
- 关系型数据库
- MongoDB
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
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])
}
查询隐式多对多关系
以下部分演示了如何查询隐式 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
属性,则不能使用references
、fields
、onUpdate
或onDelete
参数。这是因为对于隐式 m-n 关系,这些参数采用固定值,并且无法更改。 -
要求两个模型都具有单个
@id
。请注意- 您不能使用多字段 ID
- 您不能使用
@unique
代替@id
信息要使用这些特性中的任何一个,您必须使用显式 m-n 关系。
隐式 m-n 关系中关系表的约定
如果您从内省获取数据模型,您仍然可以通过遵循 Prisma ORM 的 关系表约定来使用隐式 m-n 关系。以下示例假设您想要创建一个关系表,以获取两个名为 Post
和 Category
的模型的隐式 m-n 关系。
关系表
如果您希望关系表被内省识别为隐式 m-n 关系,则名称必须遵循以下确切结构
- 它必须以下划线
_
开头 - 然后是按字母顺序排列的第一个模型的名称(在本例中为
Category
) - 然后是关系(在本例中为
To
) - 然后是按字母顺序排列的第二个模型的名称(在本例中为
Post
)
在示例中,正确的表名称是 _CategoryToPost
。
当您自己在 Prisma schema 文件中创建隐式 m-n 关系时,您可以配置关系以具有不同的名称。这将更改数据库中关系表的名称。例如,对于名为 "MyRelation"
的关系,对应的表将称为 _MyRelation
。
多模式
如果您的隐式多对多关系跨越多个数据库模式(使用 multiSchema
预览特性),则关系表(名称直接在上面定义,在示例中为 _CategoryToPost
)必须与按字母顺序排列的第一个模型(在本例中为 Category
)位于同一数据库模式中。
列
隐式 m-n 关系的关系表必须正好有两列
- 一个指向
Category
的外键列,名为A
- 一个指向
Post
的外键列,名为B
列必须命名为 A
和 B
,其中 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
属性,以及强制性的fields
和references
参数 - 双方的引用 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
属性必须在双方都定义fields
和references
参数fields
参数必须只定义一个标量字段,该字段必须是列表类型references
参数必须只定义一个标量字段。此标量字段必须存在于被引用的模型上,并且必须与fields
参数中的标量字段类型相同,但为单数(非列表)references
指向的标量字段必须具有@id
属性@relation
中不允许使用引用操作
关系型数据库中使用的隐式 m-n 关系在 MongoDB 上不受支持。
查询 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',
},
},
},
},
})