跳到主要內容

資料表繼承

概觀

資料表繼承是一種軟體設計模式,允許對實體之間的階層式關係進行建模。在資料庫層級使用資料表繼承也可以在您的 JavaScript/TypeScript 應用程式中使用 union 類型,或在多個模型之間共享一組通用屬性。

本頁介紹了兩種資料表繼承方法,並說明如何將它們與 Prisma ORM 一起使用。

資料表繼承的一個常見用例可能是應用程式需要顯示某種類型的內容活動 feed。在這種情況下,內容活動可以是影片文章。舉例來說,假設

  • 內容活動始終具有 idurl
  • 除了 idurl 之外,影片還具有 duration(建模為 Int
  • 除了 idurl 之外,文章還具有 body(建模為 String

用例

Union 類型

Union 類型是 TypeScript 中一個方便的功能,允許開發人員更靈活地使用其資料模型中的類型。

在 TypeScript 中,union 類型如下所示

type Activity = Video | Article

雖然目前無法在 Prisma schema 中對 union 類型進行建模,但您可以透過使用資料表繼承和一些額外的類型定義將它們與 Prisma ORM 一起使用。

在多個模型之間共享屬性

如果您有一個用例,其中多個模型應共享一組特定的屬性,您也可以使用資料表繼承對此進行建模。

例如,如果來自上方的 VideoArticle 模型都應該具有共享的 title 屬性,您也可以使用資料表繼承來實現此目的。

範例

在一個簡單的 Prisma schema 中,這將如下所示。請注意,我們也新增了 User 模型,以說明這如何與關聯一起運作

schema.prisma
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 資料表,其中包含 idurl 以及 durationbody 欄位。它還使用一個 type 欄位,指示活動影片還是文章
  • 多資料表繼承 (MTI):使用多個資料表分別儲存不同實體的資料,並透過外鍵將它們連結起來。在我們的範例中,將會有一個 Activity 資料表,其中包含 idurl 欄位、一個 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[]
}

一些需要注意的事項

  • 模型特定的屬性 durationbody 必須標記為選用(即,使用 ?)。這是因為 Activity 資料表中代表影片的記錄不得具有 body 的值。相反地,代表文章Activity 記錄永遠不能設定 duration
  • type 鑑別器欄位指示每個記錄代表影片還是文章項目。

Prisma Client API

由於 Prisma ORM 如何為資料模型產生類型和 API,因此只會有 Activity 類型以及屬於它的 CRUD 查詢(createupdatedelete、...)可供您使用。

查詢影片和文章

您現在可以透過篩選 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 類型的物件轉換為 VideoArticle 類型的映射函數會很有幫助

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 轉換為更具體的類型(即 ArticleVideo

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[]
}

一些需要注意的事項

  • ActivityVideo 以及 ActivityArticle 之間需要 1-1 關聯。此關聯用於在需要時提取有關記錄的特定資訊。
  • 使用這種方法,模型特定的屬性 durationbody 可以設為必填
  • type 鑑別器欄位指示每個記錄代表影片還是文章項目。

Prisma Client API

這次,您可以透過 PrismaClient 實例上的 videoarticle 屬性直接查詢影片和文章。

查詢影片和文章

如果您想存取共享屬性,您需要使用 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 產生的 VideoArticle 類型與 Activity 類型結合來定義 VideoArticle 類型。這些組合建立了一個具有所需屬性的新類型。請注意,我們也省略了 type 鑑別器欄位,因為在特定類型上不再需要它

import {
Video as VideoDB,
Article as ArticleDB,
Activity,
} from '@prisma/client'

type Video = Omit<VideoDB & Activity, 'type'>
type Article = Omit<ArticleDB & Activity, 'type'>

一旦定義了這些類型,您就可以定義映射函數,將您從上述查詢接收的類型轉換為所需的 VideoArticle 類型。以下是 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 已經為特定模型(即,上述範例中的 ArticleVideo)提供了正確的類型定義,而您需要使用 STI 從頭開始建立這些類型定義。
  • ID / 主鍵:使用 MTI,記錄有兩個 ID(父資料表上一個,子資料表上另一個),它們可能不匹配。您需要在應用程式的業務邏輯中考慮這一點。

第三方解決方案

雖然 Prisma ORM 目前尚不原生支援 union 類型或多型,但您可以查看 Zenstack,它正在為 Prisma schema 新增額外的功能層。閱讀他們的 關於 Prisma ORM 中多型的部落格文章以了解更多資訊。