表繼承
概覽
表繼承是一種軟體設計模式,允許對實體之間的層次關係進行建模。在資料庫層面使用表繼承還可以使你的 JavaScript/TypeScript 應用程式中使用聯合型別,或在多個模型之間共享一組通用屬性。
本頁介紹了兩種表繼承方法,並解釋瞭如何在 Prisma ORM 中使用它們。
表繼承的一個常見用例是當應用程式需要顯示某種內容活動的訂閱源時。在這種情況下,內容活動可以是影片或文章。舉個例子,我們假設
- 內容活動始終具有
id和url - 除了
id和url,影片還有一個duration(建模為Int) - 除了
id和url,文章還有一個body(建模為String)
用例
聯合型別
聯合型別是 TypeScript 中的一個方便的特性,它允許開發者更靈活地處理資料模型中的型別。
在 TypeScript 中,聯合型別如下所示
type Activity = Video | Article
雖然目前無法在 Prisma schema 中建模聯合型別,但你可以透過使用表繼承和一些額外的型別定義來在 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[]
}
讓我們研究一下如何使用表繼承來建模。
單表繼承 vs 多表繼承
以下是兩種主要表繼承方法的快速比較
- 單表繼承 (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之間需要一對一關係。當需要時,此關係用於獲取有關記錄的特定資訊。- 使用此方法,模型特有的屬性
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 目前不原生支援聯合型別或多型性,但你可以檢視 Zenstack,它為 Prisma schema 添加了額外功能層。閱讀他們的關於 Prisma ORM 中多型性的部落格文章以瞭解更多資訊。