今天的主題是關於 Many-to-many relations,他代表著關聯的 relation 他可以是 0 筆或是多筆資料的關聯,所以多對多的關聯的欄位他是 optional,那下面我們將更詳細介紹多對多的特點,廢話不多說我們走起~
在 relation database 中處理 m-n relation 也就是多對多的關聯是資料庫透過relation tables 去管理的,意思是透過另外一個 table 去管理兩個 table 的關聯內容,以下簡稱關係表,其中 prisma 提供 explicit 跟 implicit 兩種方式管理 schema
直接了當的把 relation 中管理多對多的情況所有的 schema 都寫出來,在關係表中 TagsOnPosts 多了 assignedAt 跟 assignedBy
model Post {
id Int @id @default(autoincrement())
title String
categories TagsOnPosts[]
}
model Tag {
id Int @id @default(autoincrement())
name String
posts TagsOnPosts[]
}
model TagsOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int // relation scalar field (used in the `@relation` attribute above)
tag Category @relation(fields: [tagId], references: [id])
tagId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, tagId])
}
你會發現一個多對多的關聯表,除了兩者 model 外,還會多一組一對多的 model ,如 _post_tag 關聯 posts 跟 tags ,這邊補充一下在 relation DB 中關聯表會是 _ 開頭當作他的 name

implicit 不代表不遵守 relation table 管理多對多的 table 的行為, 只是讓 prisma 自動幫你完成,讓你的 shcmea 更乾淨,無須多做額外的 shcmea 訂定,他可以讓你的 prisma client 使用時少一層 nested writes 如以下 create.assignedBy 跟 create.assignedAt 之後才是 create category
// explicit
const createCategory = await prisma.post.create({
data: {
title: 'How to be Bob',
tags: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
create: {
name: 'New category',
},
},
},
],
},
},
})
但在 implicit 就少一層~
// implicit
const createPostAndCategory = await prisma.post.create({
data: {
title: 'How to become a butterfly',
tags: {
create: [{ name: 'Magic' }, { name: 'Butterflies' }],
},
},
})
prisma 的 schema 中prisma 自動幫你管理 relation table,而關係表不會在 prisma schema 中出現這邊 prisma 比較推薦使用 implicit 的寫法,原因是如果你不需要在關係表中加上額外的欄位,例如何時產生關聯的,或是誰產生關係表的內容的話用 implicit 讓 prisma 自動幫你管理多對多的關係,但如果你要用 explicit 方式也是 OK 的
補充 如果要使用 implicit 的話有幾個點要注意:
multi-field ID
model Tag {
name String
type String
posts TagsOnPosts[]
@@id(fields: [name, type])
}
@unique 取代 @id
model Tag {
id Int @unique
name String
posts TagsOnPosts[]
}
儘管不會跳 prisma error ,但如果你需要用的話只能在 explicit 中使用
Explicit 中 CategoriesOnPosts 作為中間表,是可以直接在 prisma client 中去使用,你可以訂定一些額外的欄位,同時組成 Explicit 的 schema 有三個
model 分別為 Post 跟 Category 代表多對多關係CategoriesOnPosts 作為一個關係表,裏頭訂定了關聯的關係欄位 (annotated relation) post 跟 category 去對應 postId 跟 categoryId
CategoriesOnPosts 很重要的是除了能管理 Post 跟 Category 的關聯外,也會提供額外的資訊例如 assignedAt 跟 assignedBy ,讓你 create Post 或是 create Category 關聯時知道是誰創建的資料,以及何時建立的資料等等
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])
}
備註: 所以多對多得關聯表會儲存兩個 model 的 FK

上面的 schema 會產生以下的 SQL ,你會發現 CategoriesOnPosts 才會有 CONSTRAINT 去管理 FOREIGN KEY
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;
CategoriesOnPosts 則是跟一對多規則是一樣的,因為 Post↔ CategoriesOnPosts and Category ↔ CategoriesOnPosts ,所以會有 @relation 去 annotated relation ,例如 categoryId 跟 postId
如果你不需要像是 assignedAt 這種額外的欄位在關係表中的話,直接使用 implicit 寫法就好
prisma 中使用 Multi-schema _CategoryToPost 必須要跟第一個 model 的 schema 要一樣的,也就是 Post 跟 _CategoryToPost 要一樣。你會發現因為 Explicit 有多一層關係表的關係,所以在 create 會有三步驟
const createCategory = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
create: {
name: 'New category',
},
},
},
],
},
},
})
讓你一樣也可以透過 connect 寫法,去 connect 已經有的 category
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,
},
},
},
],
},
},
})
也可以使用 connectOrCreate 如果你不確定 category 是否存在
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,
},
},
},
},
],
},
},
})
這邊就是找出 category 是 New Category 的 post 你會發現因為 Explicit 關係會先有一層 categories 然後才是 category ,所以 Explicit 的寫法會有多一層 nested writes ,但如果你是 implicit 就會少這一層
const getPosts = await prisma.post.findMany({
where: {
categories: {
some: {
category: {
name: 'New Category',
},
},
},
},
})
關係表也可以用來 query data 去檢查哪個 post 是透過 Bob 去創建的
const getAssignments = await prisma.categoriesOnPosts.findMany({
where: {
assignedBy: 'Bob',
post: {
id: {
in: [9, 4, 10, 12, 22],
},
},
},
})
Implicit 跟 Explicit 的差別只會在於 prisma ,但在 SQL 中的運行是沒有差異性的,唯一的差別是 Implicit 會讓 prisma 自動幫你管理如 Explicit mode 中需要創建 CategoriesOnPosts 這個 schema 這件事,所以在 Implicit mode 中你的 prisma 的 schema 並不會出現中間表,因為 Implicit 中你不需要額外的訊息去紀錄中間表這件事,所以 Implicit 會讓你的 prisma client 使用更簡單,以下是簡單的 Implicit demo:
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
這邊就是 create 一個 post 去關聯 categories
// Implicit
const createPostAndCategory = await prisma.post.create({
data: {
title: 'How to become a butterfly',
categories: {
create: [{ name: 'Magic' }, { name: 'Butterflies' }],
},
},
})
你會發現跟 Explicit 寫起來是不是乾淨很多呢XD
// Explicit
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,
},
},
},
],
},
},
})
同樣也可以反過來去 create category
const createCategoryAndPosts = await prisma.category.create({
data: {
name: 'Stories',
posts: {
create: [
{ title: 'That one time with the stuff' },
{ title: 'The story of planet Earth' },
],
},
},
})
那如果是 get data 的話,因為 prisma 預設不會 return relation 的 data,所以如果要看到 relation data 的話記得加上 include
const getPostsAndCategories = await prisma.post.findMany({
include: {
categories: true,
},
})
@relation 除非你需要 Disambiguating relations
model User {
id Int @id @default(autoincrement())
name String?
writtenPosts Post[]
pinnedPost Post?
}
model Post {
id Int @id @default(autoincrement())
title String?
author User @relation(fields: [authorId], references: [id])
authorId Int
pinnedBy User? @relation(fields: [pinnedById], references: [id])
pinnedById Int?
}
@relation 不能使用 references、fields、onUpdate、onDelete這些 arguments 因為在 Implicit 中會採用 default value
model 必須要有 @id 不能使用 multi-field ID 的方式或是 @unique 取代 @id
預設情況下 Post 跟 Category 的中間表他的 table name 會是 _CategoryToPost ,如果你希望 customer name 的話可以加在 @relation 中
model Post {
id Int @id @default(autoincrement())
categories Category[] @relation("MyRelationTable")
}
model Category {
id Int @id @default(autoincrement())
posts Post[] @relation("MyRelationTable")
}
所以在 create 多對多的 relation table 中,在 SQL 的模型會是將關聯兩個不同的 entities ,在這個模型的本質上他會是兩個 1-n 的 relation table 組合起來的,也就是上面說的關係表的概念 (CategoriesOnPosts) 。
✅ 前端社群 :
https://lihi3.cc/kBe0Y