資料表繼承
概觀
資料表繼承是一種軟體設計模式,允許對實體之間的階層式關係進行建模。在資料庫層級使用資料表繼承也可以在您的 JavaScript/TypeScript 應用程式中使用 union 類型,或在多個模型之間共享一組通用屬性。
本頁介紹了兩種資料表繼承方法,並說明如何將它們與 Prisma ORM 一起使用。
資料表繼承的一個常見用例可能是應用程式需要顯示某種類型的內容活動的 feed。在這種情況下,內容活動可以是影片或文章。舉例來說,假設
- 內容活動始終具有
id
和url
- 除了
id
和url
之外,影片還具有duration
(建模為Int
) - 除了
id
和url
之外,文章還具有body
(建模為String
)
用例
Union 類型
Union 類型是 TypeScript 中一個方便的功能,允許開發人員更靈活地使用其資料模型中的類型。
在 TypeScript 中,union 類型如下所示
type Activity = Video | Article
雖然目前無法在 Prisma schema 中對 union 類型進行建模,但您可以透過使用資料表繼承和一些額外的類型定義將它們與 Prisma ORM 一起使用。
在多個模型之間共享屬性
如果您有一個用例,其中多個模型應共享一組特定的屬性,您也可以使用資料表繼承對此進行建模。
例如,如果來自上方的 Video
和 Article
模型都應該具有共享的 title
屬性,您也可以使用資料表繼承來實現此目的。
範例
在一個簡單的 Prisma schema 中,這將如下所示。請注意,我們也新增了 User
模型,以說明這如何與關聯一起運作
model Video {
id Int @id
url String @unique
duration Int
user User @relation(fields: [userId], references: [id])
userId Int
}
model Article {
id Int @id
url String @unique
body String
user User @relation(fields: [userId], references: [id])
userId Int
}
model User {
id Int @id
name String
videos Video[]
articles Article[]
}
讓我們研究一下如何使用資料表繼承對此進行建模。
單一資料表與多資料表繼承
以下是資料表繼承兩種主要方法的快速比較
- 單一資料表繼承 (STI):使用單一資料表在一個位置儲存所有不同實體的資料。在我們的範例中,將會有一個單一的
Activity
資料表,其中包含id
、url
以及duration
和body
欄位。它還使用一個type
欄位,指示活動是影片還是文章。 - 多資料表繼承 (MTI):使用多個資料表分別儲存不同實體的資料,並透過外鍵將它們連結起來。在我們的範例中,將會有一個
Activity
資料表,其中包含id
、url
欄位、一個Video
資料表,其中包含duration
和指向Activity
的外鍵,以及一個Article
資料表,其中包含body
和外鍵。還有一個type
欄位,充當鑑別器,指示活動是影片還是文章。請注意,多資料表繼承有時也稱為委派類型。
您可以下方了解兩種方法的權衡。
單一資料表繼承 (STI)
資料模型
使用 STI,上述情境可以建模如下
model Activity {
id Int @id // shared
url String @unique // shared
duration Int? // video-only
body String? // article-only
type ActivityType // discriminator
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
}
enum ActivityType {
Video
Article
}
model User {
id Int @id @default(autoincrement())
name String?
activities Activity[]
}
一些需要注意的事項
- 模型特定的屬性
duration
和body
必須標記為選用(即,使用?
)。這是因為Activity
資料表中代表影片的記錄不得具有body
的值。相反地,代表文章的Activity
記錄永遠不能設定duration
。 type
鑑別器欄位指示每個記錄代表影片還是文章項目。
Prisma Client API
由於 Prisma ORM 如何為資料模型產生類型和 API,因此只會有 Activity
類型以及屬於它的 CRUD 查詢(create
、update
、delete
、...)可供您使用。
查詢影片和文章
您現在可以透過篩選 type
欄位來僅查詢影片或文章。例如
// Query all videos
const videos = await prisma.activity.findMany({
where: { type: 'Video' },
})
// Query all articles
const articles = await prisma.activity.findMany({
where: { type: 'Article' },
})
定義專用類型
當像這樣查詢影片和文章時,TypeScript 仍然只會識別 Activity
類型。這可能會很煩人,因為即使是 videos
中的物件也會具有(選用的)body
,而 articles
中的物件也會具有(選用的)duration
欄位。
如果您想要這些物件的類型安全,您需要為它們定義專用類型。例如,您可以透過使用產生的 Activity
類型和 TypeScript Omit
实用程序類型來從中刪除屬性來做到這一點
import { Activity } from '@prisma/client'
type Video = Omit<Activity, 'body' | 'type'>
type Article = Omit<Activity, 'duration' | 'type'>
此外,建立將 Activity
類型的物件轉換為 Video
和 Article
類型的映射函數會很有幫助
function activityToVideo(activity: Activity): Video {
return {
url: activity.url,
duration: activity.duration ? activity.duration : -1,
ownerId: activity.ownerId,
} as Video
}
function activityToArticle(activity: Activity): Article {
return {
url: activity.url,
body: activity.body ? activity.body : '',
ownerId: activity.ownerId,
} as Article
}
現在,您可以在查詢後將 Activity
轉換為更具體的類型(即 Article
或 Video
)
const videoActivities = await prisma.activity.findMany({
where: { type: 'Video' },
})
const videos: Video[] = videoActivities.map(activityToVideo)
使用 Prisma Client 擴充功能以獲得更方便的 API
您可以使用Prisma Client 擴充功能為資料庫中的資料表結構建立更方便的 API。
多資料表繼承 (MTI)
資料模型
使用 MTI,上述情境可以建模如下
model Activity {
id Int @id @default(autoincrement())
url String // shared
type ActivityType // discriminator
video Video? // model-specific 1-1 relation
article Article? // model-specific 1-1 relation
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
}
model Video {
id Int @id @default(autoincrement())
duration Int // video-only
activityId Int @unique
activity Activity @relation(fields: [activityId], references: [id])
}
model Article {
id Int @id @default(autoincrement())
body String // article-only
activityId Int @unique
activity Activity @relation(fields: [activityId], references: [id])
}
enum ActivityType {
Video
Article
}
model User {
id Int @id @default(autoincrement())
name String?
activities Activity[]
}
一些需要注意的事項
Activity
和Video
以及Activity
和Article
之間需要 1-1 關聯。此關聯用於在需要時提取有關記錄的特定資訊。- 使用這種方法,模型特定的屬性
duration
和body
可以設為必填。 type
鑑別器欄位指示每個記錄代表影片還是文章項目。
Prisma Client API
這次,您可以透過 PrismaClient
實例上的 video
和 article
屬性直接查詢影片和文章。
查詢影片和文章
如果您想存取共享屬性,您需要使用 include
來提取與 Activity
的關聯。
// Query all videos
const videos = await prisma.video.findMany({
include: { activity: true },
})
// Query all articles
const articles = await prisma.article.findMany({
include: { activity: true },
})
根據您的需求,您也可以透過篩選 type
鑑別器欄位來反向查詢
// Query all videos
const videoActivities = await prisma.activity.findMany({
where: { type: 'Video' }
include: { video: true }
})
定義專用類型
與 STI 相比,在類型方面雖然更方便一些,但產生的類型定義可能仍然無法滿足您的所有需求。
以下是如何透過將 Prisma ORM 產生的 Video
和 Article
類型與 Activity
類型結合來定義 Video
和 Article
類型。這些組合建立了一個具有所需屬性的新類型。請注意,我們也省略了 type
鑑別器欄位,因為在特定類型上不再需要它
import {
Video as VideoDB,
Article as ArticleDB,
Activity,
} from '@prisma/client'
type Video = Omit<VideoDB & Activity, 'type'>
type Article = Omit<ArticleDB & Activity, 'type'>
一旦定義了這些類型,您就可以定義映射函數,將您從上述查詢接收的類型轉換為所需的 Video
和 Article
類型。以下是 Video
類型的範例
import { Prisma, Video as VideoDB, Activity } from '@prisma/client'
type Video = Omit<VideoDB & Activity, 'type'>
// Create `VideoWithActivity` typings for the objects returned above
const videoWithActivity = Prisma.validator<Prisma.VideoDefaultArgs>()({
include: { activity: true },
})
type VideoWithActivity = Prisma.VideoGetPayload<typeof videoWithActivity>
// Map to `Video` type
function toVideo(a: VideoWithActivity): Video {
return {
id: a.id,
url: a.activity.url,
ownerId: a.activity.ownerId,
duration: a.duration,
activityId: a.activity.id,
}
}
現在,您可以取得上述查詢傳回的物件,並使用 toVideo
轉換它們
const videoWithActivities = await prisma.video.findMany({
include: { activity: true },
})
const videos: Video[] = videoWithActivities.map(toVideo)
使用 Prisma Client 擴充功能以獲得更方便的 API
您可以使用Prisma Client 擴充功能為資料庫中的資料表結構建立更方便的 API。
STI 和 MTI 之間的權衡
- 資料模型:使用 MTI,資料模型可能會感覺更乾淨。使用 STI,您最終可能會得到非常寬的列和許多在其中具有
NULL
值的欄位。 - 效能:MTI 可能會帶來效能成本,因為您需要聯結父資料表和子資料表才能存取模型所有相關的屬性。
- 類型定義:使用 Prisma ORM,MTI 已經為特定模型(即,上述範例中的
Article
和Video
)提供了正確的類型定義,而您需要使用 STI 從頭開始建立這些類型定義。 - ID / 主鍵:使用 MTI,記錄有兩個 ID(父資料表上一個,子資料表上另一個),它們可能不匹配。您需要在應用程式的業務邏輯中考慮這一點。
第三方解決方案
雖然 Prisma ORM 目前尚不原生支援 union 類型或多型,但您可以查看 Zenstack,它正在為 Prisma schema 新增額外的功能層。閱讀他們的 關於 Prisma ORM 中多型的部落格文章以了解更多資訊。