跳到主要內容

多對多關聯

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

以下查詢傳回所有 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 關聯使 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 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',
},
},
},
},
})