今天的主題是關於 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