多對多關係
多對多(m-n)關係是指關係的一側的零個或多個記錄可以連線到另一側的零個或多個記錄的關係。
Prisma schema 語法及其在底層資料庫中的實現因關係型資料庫和MongoDB而異。
關係型資料庫
在關係型資料庫中,多對多關係通常透過關係表建模。多對多關係在 Prisma schema 中可以是顯式的或隱式的。如果您不需要在關係表中儲存任何額外的元資料,我們建議使用隱式多對多關係。如果需要,您以後可以隨時遷移到顯式多對多關係。
顯式多對多關係
在顯式多對多關係中,關係表在 Prisma schema 中被表示為一個模型,並可以在查詢中使用。顯式多對多關係定義了三個模型
- 兩個具有多對多關係的模型,例如
Category和Post。 - 一個表示關係表的模型,例如底層資料庫中的
CategoriesOnPosts(有時也稱為連線表、連結表或樞紐表)。關係表模型的欄位既是帶註解的關係欄位(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;
請注意,與一對多關係相同的規則適用(因為 Post↔ CategoriesOnPosts 和 Category ↔ CategoriesOnPosts 實際上都是一對多關係),這意味著關係的一側需要用 @relation 屬性進行註解。
當您不需要向關係附加額外資訊時,可以將多對多關係建模為隱式多對多關係。如果您不使用 Prisma Migrate 而是從內省獲取資料模型,您仍然可以透過遵循 Prisma ORM 的關係表約定來使用隱式多對多關係。
查詢顯式多對多關係
以下部分演示如何查詢顯式多對多關係。您可以直接查詢關係模型(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],
},
},
},
})
隱式多對多關係
隱式多對多關係將關係欄位定義為關係兩側的列表。儘管關係表存在於底層資料庫中,但它由 Prisma ORM 管理,並且不會在 Prisma schema 中顯現。隱式關係表遵循特定約定。
隱式多對多關係使多對多關係的 Prisma Client API 稍微簡化(因為巢狀寫入中的巢狀層級少了一層)。
在下面的示例中,Post 和 Category 之間存在一個隱式多對多關係
- 關係型資料庫
- 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])
}
查詢隱式多對多關係
以下部分演示如何查詢隱式多對多關係。與顯式多對多查詢相比,這些查詢需要的巢狀層級更少。
以下查詢建立一個 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,
},
})
定義隱式多對多關係的規則
隱式多對多關係
-
使用關係表的特定約定
-
除非您需要使用名稱消除關係歧義,否則不需要
@relation屬性,例如@relation("MyRelation")或@relation(name: "MyRelation")。 -
如果您確實使用了
@relation屬性,則不能使用references、fields、onUpdate或onDelete引數。這是因為對於隱式多對多關係,這些引數具有固定值,無法更改。 -
要求兩個模型都有一個單一的
@id。請注意,- 您不能使用多欄位 ID
- 您不能使用
@unique代替@id
資訊要使用這些功能中的任何一個,您必須使用顯式多對多關係。
隱式多對多關係中關係表的約定
如果您從內省獲取資料模型,您仍然可以透過遵循 Prisma ORM 的關係表約定來使用隱式多對多關係。以下示例假設您希望建立一個關係表,以獲取兩個名為 Post 和 Category 的模型的隱式多對多關係。
關係表
如果您希望關係表被內省識別為隱式多對多關係,則其名稱必須遵循此精確結構
- 必須以一個下劃線
_開頭 - 然後是按字母順序排列的第一個模型的名稱(本例中為
Category) - 然後是關係(本例中為
To) - 然後是按字母順序排列的第二個模型的名稱(本例中為
Post)
在此示例中,正確的表名稱是 _CategoryToPost。
在 Prisma schema 檔案中自行建立隱式多對多關係時,您可以配置關係以使用不同的名稱。這將改變資料庫中關係表的名稱。例如,對於名為 "MyRelation" 的關係,對應的表將被稱為 _MyRelation。
多 Schema
如果您的隱式多對多關係跨越多個數據庫 schema(使用multiSchema 預覽特性),則關係表(名稱如上定義,本例中為 _CategoryToPost)必須與按字母順序排列的第一個模型(本例中為 Category)位於同一資料庫 schema 中。
列
隱式多對多關係的關係表必須且僅有兩個列
- 一個指向
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 內省會將其識別為隱式多對多關係
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")
}
關係表
關係表(有時也稱為連線表、連結表或樞紐表)連線兩個或多個其他表,從而在它們之間建立關係。建立關係表是 SQL 中一種常見的資料建模實踐,用於表示不同實體之間的關係。本質上,這意味著“一個多對多關係在資料庫中被建模為兩個一對多關係”。
我們建議使用隱式多對多關係,其中 Prisma ORM 會自動在底層資料庫中生成關係表。顯式多對多關係應用於需要在關係中儲存額外資料的情況,例如關係建立日期。
MongoDB
在 MongoDB 中,多對多關係由以下方式表示
- 關係兩側的關係欄位,每個欄位都具有
@relation屬性,並帶有強制性的fields和references引數 - 關係兩側的引用 ID 的標量列表,其型別與另一側的 ID 欄位匹配
以下示例演示了帖子和類別之間的多對多關係
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 中的多對多關係
- 關係兩側的欄位必須具有列表型別(在上面的示例中,
categories的型別為Category[],posts的型別為Post[]) @relation屬性必須在兩側定義fields和references引數fields引數必須只定義一個標量欄位,且該欄位必須是列表型別references引數必須只定義一個標量欄位。此標量欄位必須存在於引用的模型上,並且必須與fields引數中的標量欄位型別相同,但為單數(非列表)references指向的標量欄位必須具有@id屬性@relation中不允許使用參照操作
關係型資料庫中使用的隱式多對多關係在 MongoDB 上不受支援。
查詢 MongoDB 多對多關係
本節演示如何使用上述示例 schema 查詢 MongoDB 中的多對多關係。
以下查詢查詢具有特定匹配類別 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',
},
},
},
},
})