跳至主要内容

表继承

概述

表继承是一种软件设计模式,它允许对实体之间的层次关系进行建模。在数据库级别使用表继承还可以使您的 JavaScript/TypeScript 应用程序能够使用联合类型,或者在多个模型之间共享一组公共属性。

本页介绍了两种表继承方法,并解释了如何在 Prisma ORM 中使用它们。

表继承的常见用例可能是当应用程序需要显示某种内容活动提要时。在这种情况下,内容活动可以是视频文章。例如,假设

  • 内容活动始终具有idurl
  • 除了idurl外,视频还具有duration(建模为Int
  • 除了idurl外,文章还具有body(建模为String

用例

联合类型

联合类型是 TypeScript 中的一个方便功能,它允许开发人员更灵活地处理其数据模型中的类型。

在 TypeScript 中,联合类型如下所示

type Activity = Video | Article

虽然目前无法在 Prisma 模式中建模联合类型,但您可以通过使用表继承和一些额外的类型定义在 Prisma ORM 中使用它们。

在多个模型之间共享属性

如果您有一个用例,其中多个模型应该共享一组特定的属性,您也可以使用表继承来对其进行建模。

例如,如果上面的VideoArticle模型都应该具有共享的title属性,您也可以使用表继承来实现这一点。

示例

在一个简单的 Prisma 模式中,这将如下所示。请注意,我们还添加了一个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 客户端 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 客户端扩展来获得更方便的 API

您可以使用Prisma 客户端扩展为数据库中的表结构创建更方便的 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之间需要一对一关系。此关系用于在需要时获取有关记录的特定信息。
  • 模型特定的属性durationbody可以使用这种方法设置为必需
  • type鉴别器列指示每个记录是代表视频还是文章项。

Prisma 客户端 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 客户端扩展为数据库中的表结构创建更方便的 API。

STI 和 MTI 之间的权衡

  • 数据模型:使用 MTI,数据模型可能感觉更简洁。使用 STI,您最终可能会得到非常宽的行,以及包含大量 NULL 值的列。
  • 性能:MTI 可能会有性能开销,因为您需要连接父表和子表才能访问与模型相关的所有属性。
  • 类型:使用 Prisma ORM,MTI 为您提供了特定模型(例如,上面示例中的 ArticleVideo)的正确类型,而使用 STI,您需要从头开始创建这些类型。
  • ID / 主键:使用 MTI,记录有两个 ID(一个在父表上,另一个在子表上),它们可能不匹配。您需要在应用程序的业务逻辑中考虑这一点。

第三方解决方案

虽然 Prisma ORM 目前不支持联合类型或多态性,但您可以查看 Zenstack,它为 Prisma 模式添加了额外的功能层。阅读他们的 关于 Prisma ORM 中多态性的博客文章 以了解更多信息。