Prisma ORM 是 ORM 吗?
简单来说:是的,Prisma ORM 是一种新型的 ORM,它与传统的 ORM 根本不同,并且没有传统 ORM 常见的许多问题。
传统的 ORM 提供了一种面向对象的方式来处理关系型数据库,通过将表映射到编程语言中的模型类。这种方法会导致许多问题,这些问题是由对象-关系阻抗不匹配引起的。
Prisma ORM 的工作原理与此根本不同。使用 Prisma ORM,您可以在声明式的Prisma schema中定义模型,该 schema 是您的数据库 schema 和编程语言中模型的单一事实来源。然后在您的应用程序代码中,可以使用 Prisma Client 以类型安全的方式读写数据库中的数据,而无需管理复杂的模型实例所带来的开销。这使得查询数据的过程更加自然和可预测,因为 Prisma Client 始终返回普通的 JavaScript 对象。
在本文中,您将更详细地了解 ORM 模式和工作流程、Prisma ORM 如何实现 Data Mapper 模式以及 Prisma ORM 方法的优势。
什么是 ORM?
如果您已经熟悉 ORM,请随意跳到下一节关于 Prisma ORM 的内容。
ORM 模式 - Active Record 和 Data Mapper
ORM 提供了高层级的数据库抽象。它们通过对象暴露了一个程序化接口,用于创建、读取、删除和操作数据,同时隐藏了数据库的一些复杂性。
ORM 的想法是您将模型定义为映射到数据库表的类。这些类及其实例为您提供了在数据库中读写数据的程序化 API。
有两种常见的 ORM 模式:Active Record 和Data Mapper,它们在对象和数据库之间传输数据的方式上有所不同。虽然这两种模式都要求您将类定义为主要构建块,但两者之间最显著的区别是 Data Mapper 模式将应用程序代码中的内存对象与数据库解耦,并使用数据映射器层在两者之间传输数据。实际上,这意味着使用 Data Mapper 时,内存中的对象(表示数据库中的数据)甚至不知道数据库的存在。
Active Record
Active Record ORM 将模型类映射到数据库表,其中两种表示的结构紧密相关,例如,模型类中的每个字段在数据库表中都会有一个匹配的列。模型类的实例封装了数据库行,并携带数据和访问逻辑以处理数据库中的持久化更改。此外,模型类还可以携带特定于模型中数据的业务逻辑。
模型类通常具有以下方法
- 从 SQL 查询构造模型的实例。
- 构造一个新的实例以便稍后插入到表中。
- 封装常用的 SQL 查询并返回 Active Record 对象。
- 更新数据库并将 Active Record 中的数据插入到数据库中。
- 获取和设置字段。
- 实现业务逻辑。
Data Mapper
与 Active Record 相反,Data Mapper ORM 将应用程序中数据的内存表示与数据库的表示解耦。通过要求您将映射职责分离到两种类型的类中来实现解耦
- 实体类:应用程序中实体的内存表示,它们不知道数据库的存在
- 映射器类:它们有两个职责
- 在两种表示之间转换数据。
- 生成从数据库获取数据并将更改持久化到数据库中所需的 SQL。
Data Mapper ORM 允许在代码实现的领域问题和数据库之间有更大的灵活性。这是因为数据映射器模式允许您隐藏数据库的实现方式,这并不是在整个数据映射层背后思考您领域问题的理想方式。
传统数据映射器 ORM 这样做的原因之一是组织的结构,其中这两个职责由不同的团队处理,例如 DBA 和后端开发人员。
实际上,并非所有 Data Mapper ORM 都严格遵守此模式。例如,TypeORM 是 TypeScript 生态系统中流行的 ORM,它支持 Active Record 和 Data Mapper,其对 Data Mapper 的处理方法如下
- 实体类使用装饰器(@Column)将类属性映射到表列,并且知道数据库的存在。
- 代替映射器类,存储库类用于查询数据库,并可能包含自定义查询。存储库使用装饰器来确定实体属性和数据库列之间的映射。
假设数据库中有以下 User 表
相应的实体类如下所示
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
}
Schema 迁移工作流程
开发使用数据库的应用程序的一个核心部分是更改数据库 schema 以适应新功能并更好地契合您正在解决的问题。在本节中,我们将讨论schema 迁移是什么以及它们如何影响工作流程。
由于 ORM 位于开发人员和数据库之间,大多数 ORM 都提供了迁移工具来协助创建和修改数据库 schema。
迁移是将数据库 schema 从一个状态转换为另一个状态的一系列步骤。第一次迁移通常创建表和索引。随后的迁移可能会添加或删除列、引入新的索引或创建新的表。根据迁移工具的不同,迁移可能采用 SQL 语句的形式,也可能采用将被转换为 SQL 语句的程序化代码(如 ActiveRecord 和 SQLAlchemy)。
由于数据库通常包含数据,迁移有助于您将 schema 更改分解为更小的单元,这有助于避免意外数据丢失。
假设您从头开始一个项目,完整的工作流程如下:您创建一个迁移,该迁移将在数据库 schema 中创建 User 表,并定义如上例所示的 User 实体类。
然后,随着项目进展,当您决定要向 User 表添加新的 salutation 列时,您将创建另一个迁移,该迁移将更改该表并添加 salutation 列。
让我们看看使用 TypeORM 迁移会是什么样子
import { MigrationInterface, QueryRunner } from 'typeorm'
export class UserRefactoring1604448000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ADD COLUMN "salutation" TEXT`)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "salutation"`)
}
}
一旦执行了迁移并且数据库 schema 已更改,实体类和映射器类也必须更新以适应新的 salutation 列。
使用 TypeORM,这意味着向 User 实体类添加一个 salutation 属性
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
@Column()
salutation: string
}
同步此类更改对 ORM 来说可能是一个挑战,因为更改是手动应用的,并且不容易通过程序验证。重命名现有列可能更加繁琐,并且涉及搜索和替换对该列的引用。
注意: Django 的 makemigrations CLI 通过检查模型中的更改来生成迁移,这与 Prisma ORM 类似,消除了同步问题。
总而言之,schema 的演进是构建应用程序的关键部分。使用 ORM,更新 schema 的工作流程涉及使用迁移工具创建迁移,然后更新相应的实体类和映射器类(取决于实现)。如您所见,Prisma ORM 对此采取了不同的方法。
既然您已经了解了迁移是什么以及它们如何融入开发工作流程,接下来您将了解更多关于 ORM 的优缺点。
ORM 的优势
开发人员选择使用 ORM 的原因各不相同
- ORM 有助于实现领域模型。领域模型是结合了业务逻辑行为和数据的对象模型。换句话说,它让您可以专注于实际的业务概念,而不是数据库结构或 SQL 语义。
- ORM 有助于减少代码量。它们使您无需为常见的 CRUD(创建、读取、更新、删除)操作编写重复的 SQL 语句,也无需转义用户输入以防止 SQL 注入等漏洞。
- ORM 要求您编写很少甚至不写 SQL(取决于您的复杂性,您可能仍然需要编写一些原始查询)。这对于不熟悉 SQL 但仍想使用数据库的开发人员来说是有益的。
- 许多 ORM 抽象了数据库特定的细节。理论上,这意味着 ORM 可以更容易地从一个数据库更改到另一个数据库。需要注意的是,在实践中,应用程序很少更改它们使用的数据库。
与所有旨在提高生产力的抽象一样,使用 ORM 也有缺点。
ORM 的缺点
ORM 的缺点在使用之初并不总是很明显。本节涵盖了一些公认的缺点
- 使用 ORM,您会形成数据库表的对象图表示,这可能导致对象-关系阻抗不匹配。当您解决的问题形成了复杂的对象图而无法轻易地映射到关系型数据库时,就会发生这种情况。在数据的两种不同表示之间进行同步是非常困难的,一种在关系型数据库中,另一种在内存中(以对象形式)。这是因为与关系型数据库记录相比,对象的相互关联方式更加灵活多样。
- 虽然 ORM 处理与问题相关的复杂性,但同步问题并没有消失。数据库 schema 或数据模型的任何更改都需要将更改映射回另一端。这种负担通常落在开发人员身上。在团队协作项目的背景下,数据库 schema 更改需要协调。
- 由于 ORM 封装了复杂性,它们的 API 表面通常很大。不用编写 SQL 的另一面是您花费大量时间学习如何使用 ORM。这适用于大多数抽象,但是如果不了解数据库的工作原理,改进慢速查询可能会很困难。
- 由于 SQL 提供的灵活性,一些复杂查询不受 ORM 支持。通过原始 SQL 查询功能可以缓解此问题,在该功能中,您将 SQL 语句字符串传递给 ORM,ORM 会为您运行查询。
现在已经涵盖了 ORM 的成本和收益,您可以更好地理解 Prisma ORM 是什么以及它如何适应。
Prisma ORM
Prisma ORM 是新一代 ORM,它使应用程序开发人员能够轻松地使用数据库,并包含以下工具
- Prisma Client:自动生成且类型安全的数据库客户端,用于您的应用程序。
- Prisma Migrate:声明式数据建模和迁移工具。
- Prisma Studio:用于浏览和管理数据库中数据的现代化 GUI。
注意:由于 Prisma Client 是最主要的工具,我们通常将其简称为 Prisma。
这三个工具使用Prisma schema作为数据库 schema、应用程序对象 schema 以及两者之间映射的单一事实来源。它由您定义,是您配置 Prisma ORM 的主要方式。
Prisma ORM 通过类型安全、丰富的自动补全和自然的关联查询 API 等功能,让您在构建软件时高效且自信。
在下一节中,您将了解 Prisma ORM 如何实现 Data Mapper ORM 模式。
Prisma ORM 如何实现 Data Mapper 模式
如本文前面所述,Data Mapper 模式非常适合数据库和应用程序由不同团队拥有的组织。
随着现代云环境的兴起,托管数据库服务和 DevOps 实践的普及,越来越多的团队采用跨职能方法,即团队拥有包括数据库和运维事项在内的整个开发周期。
Prisma ORM 使得数据库 schema 和对象 schema 可以同步演进,从而首先减少了偏差的需要,同时仍然允许您使用 @map
属性使应用程序和数据库保持一定程度的解耦。虽然这可能看起来像一个限制,但它阻止了领域模型(通过对象 schema)的演进在事后强加到数据库上。
为了理解 Prisma ORM 的 Data Mapper 模式实现在概念上与传统 Data Mapper ORM 有何不同,这里对它们的概念和构建块进行简要比较
概念 | 描述 | 传统 ORM 中的构建块 | Prisma ORM 中的构建块 | Prisma ORM 中的事实来源 |
---|---|---|---|---|
对象 schema | 应用程序中的内存数据结构 | 模型类 | 生成的 TypeScript 类型 | Prisma schema 中的模型 |
Data Mapper | 在对象 schema 和数据库之间进行转换的代码 | 映射器类 | Prisma Client 中生成的函数 | Prisma schema 中的 @map 属性 |
数据库 schema | 数据库中数据的结构,例如表和列 | 手动编写的 SQL 或通过程序化 API 生成的 SQL | Prisma Migrate 生成的 SQL | Prisma schema |
Prisma ORM 与 Data Mapper 模式一致,并具有以下附加优势
- 通过基于 Prisma schema 生成 Prisma Client,减少了定义类和映射逻辑的样板代码。
- 消除了应用程序对象和数据库 schema 之间的同步挑战。
- 数据库迁移是一等公民,因为它们派生自 Prisma schema。
既然我们已经讨论了 Prisma ORM 的 Data Mapper 方法背后的概念,接下来我们可以看看 Prisma schema 在实践中是如何工作的。
Prisma schema
Prisma 实现 Data Mapper 模式的核心是 Prisma schema – 以下职责的单一事实来源
- 配置 Prisma 如何连接到您的数据库。
- 生成 Prisma Client – 用于应用程序代码的类型安全的 ORM。
- 使用 Prisma Migrate 创建和演进数据库 schema。
- 定义应用程序对象和数据库列之间的映射。
Prisma ORM 中的模型与 Active Record ORM 中的模型含义略有不同。在 Prisma ORM 中,模型在 Prisma schema 中定义为抽象实体,描述了表、关系以及列与 Prisma Client 中属性之间的映射。
例如,这是一个博客的 Prisma schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
以下是上面示例的分解说明
datasource
块定义了与数据库的连接。generator
块告诉 Prisma ORM 为 TypeScript 和 Node.js 生成 Prisma Client。Post
和User
模型映射到数据库表。- 这两个模型具有一对多(1-n)关系,其中每个
User
可以拥有许多相关的Post
。 - 模型中的每个字段都有一个类型,例如
id
的类型是Int
。 - 字段可能包含字段属性来定义
- 使用
@id
属性定义主键。 - 使用
@unique
属性定义唯一键。 - 使用
@default
属性定义默认值。 - 使用
@map
属性定义表列和 Prisma Client 字段之间的映射,例如,content
字段(可在 Prisma Client 中访问)映射到post_content
数据库列。
- 使用
User / Post 关系可以用下图可视化
在 Prisma ORM 层面,User / Post 关系由以下部分组成
- 标量字段
authorId
,由@relation
属性引用。此字段存在于数据库表中 – 它是连接 Post 和 User 的外键。 - 两个关系字段:
author
和posts
不存在于数据库表中。关系字段在 Prisma ORM 层面定义模型之间的连接,并且只存在于 Prisma schema 和生成的 Prisma Client 中,在那里它们用于访问关系。
Prisma schema 的声明性特性简洁明了,允许定义数据库 schema 和 Prisma Client 中相应的表示。
在下一节中,您将了解 Prisma ORM 支持的工作流程。
Prisma ORM 工作流程
Prisma ORM 的工作流程与传统 ORM 略有不同。您可以在从头构建新应用程序时使用 Prisma ORM,也可以逐步采用它
- 新应用程序(绿地项目):尚未拥有数据库 schema 的项目可以使用 Prisma Migrate 创建数据库 schema。
- 现有应用程序(棕地项目):已经拥有数据库 schema 的项目可以通过 Prisma ORM自省以生成 Prisma schema 和 Prisma Client。此用例适用于任何现有的迁移工具,并且对于逐步采用很有用。可以切换到使用 Prisma Migrate 作为迁移工具。但这并非强制要求。
在这两种工作流程中,Prisma schema 都是主要的配置文件。
在现有数据库项目中逐步采用的工作流程
棕地项目通常已经拥有一些数据库抽象和 schema。Prisma ORM 可以通过自省现有数据库来获取反映现有数据库 schema 的 Prisma schema,并生成 Prisma Client,从而与这些项目集成。此工作流程与您可能正在使用的任何迁移工具和 ORM 兼容。如果您希望逐步评估和采用,此方法可以作为并行采用策略的一部分。
与此工作流程兼容的非穷尽设置列表
- 使用包含
CREATE TABLE
和ALTER TABLE
的普通 SQL 文件创建和修改数据库 schema 的项目。 - 使用第三方迁移库(如 db-migrate 或 Umzug)的项目。
- 已在使用 ORM 的项目。在这种情况下,通过 ORM 的数据库访问保持不变,而生成的 Prisma Client 可以逐步采用。
实际上,自省现有数据库并生成 Prisma Client 需要以下步骤
- 创建一个
schema.prisma
文件,定义datasource
(在这种情况下,您的现有数据库)和generator
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}
generator client {
provider = "prisma-client-js"
}
- 运行
prisma db pull
,根据您的数据库 schema 填充 Prisma schema 的模型。 - (可选)自定义 Prisma Client 和数据库之间的字段和模型映射。
- 运行
prisma generate
。
Prisma ORM 将在 node_modules
文件夹内生成 Prisma Client,您可以从中导入到您的应用程序。有关更详细的使用文档,请参阅Prisma Client API文档。
总结一下,Prisma Client 可以作为并行采用策略的一部分集成到具有现有数据库和工具的项目中。新项目将使用下一节详细介绍的不同工作流程。
新项目工作流程
Prisma ORM 在支持的工作流程方面与 ORM 不同。仔细查看创建和更改新数据库 schema 所需的步骤有助于理解 Prisma Migrate。
Prisma Migrate 是一个用于声明式数据建模和迁移的 CLI。与大多数作为 ORM 一部分的迁移工具不同,您只需要描述当前的 schema,而不是描述从一个状态迁移到另一个状态的操作。Prisma Migrate 会推断出操作,生成 SQL 并为您执行迁移。
此示例演示了在具有新数据库 schema 的新项目中使用 Prisma ORM,类似于上面的博客示例
- 创建 Prisma schema
// schema.prisma
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
- 运行
prisma migrate
生成迁移所需的 SQL,将其应用到数据库,并生成 Prisma Client。
对于数据库 schema 的任何进一步更改
- 对 Prisma schema 应用更改,例如,向 User 模型添加一个
registrationDate
字段 - 再次运行
prisma migrate
。
最后一步演示了声明式迁移的工作方式:通过向 Prisma schema 添加一个字段,并使用 Prisma Migrate 将数据库 schema 转换到所需状态。迁移运行后,Prisma Client 会自动重新生成,以反映更新后的 schema。
如果您不想使用 Prisma Migrate,但仍希望在新项目中使用类型安全的生成的 Prisma Client,请参阅下一节。
不使用 Prisma Migrate 的新项目替代方案
在新项目中使用 Prisma Client 并结合第三方迁移工具(而不是 Prisma Migrate)是可行的。例如,一个新项目可以选择使用 Node.js 迁移框架 db-migrate 来创建数据库 schema 和迁移,并使用 Prisma Client 进行查询。本质上,这属于现有数据库工作流程的范畴。
使用 Prisma Client 访问数据
到目前为止,本文已经介绍了 Prisma ORM 的概念、它如何实现 Data Mapper 模式以及它支持的工作流程。在最后一部分中,您将了解如何使用 Prisma Client 在应用程序中访问数据。
使用 Prisma Client 访问数据库是通过其公开的查询方法进行的。所有查询都返回普通的 JavaScript 对象。根据上面的博客 schema,获取用户的示例如下
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
})
在此查询中,findUnique()
方法用于从 User 表中获取单行数据。默认情况下,Prisma ORM 将返回 User 表中的所有标量字段。
注意:本示例使用 TypeScript 以充分利用 Prisma Client 提供的类型安全特性。但是,Prisma ORM 也支持在 Node.js 中的 JavaScript。
Prisma Client 通过根据 Prisma schema 生成代码,将查询和结果映射到结构类型。这意味着在生成的 Prisma Client 中,user
会有一个关联的类型。
export type User = {
id: number
email: string
name: string | null
}
这确保了访问不存在的字段会引发类型错误。更广泛地说,这意味着每次查询的结果类型在运行查询之前就已知,这有助于捕获错误。例如,以下代码片段会引发类型错误:
console.log(user.lastName) // Property 'lastName' does not exist on type 'User'.
关联查询
使用 Prisma Client 查询关联是通过 include
选项完成的。例如,查询用户及其帖子可以按如下方式完成:
const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
include: {
posts: true,
},
})
通过此查询,user
的类型也将包含 Post
,可以通过 posts
数组字段访问。
console.log(user.posts[0].title)
这个示例只触及了 Prisma Client 用于CRUD 操作的 API 的皮毛,您可以在文档中了解更多信息。核心思想是所有查询和结果都有类型支持,并且您可以完全控制如何查询关联。
结论
总而言之,Prisma ORM 是一种新型的数据映射器 ORM,它不同于传统的 ORM,并且不会遭受与之相关的常见问题。
与传统 ORM 不同,使用 Prisma ORM,您可以定义 Prisma schema——一个声明性的单一真相来源,用于表示数据库 schema 和应用程序模型。Prisma Client 中的所有查询都返回普通 JavaScript 对象,这使得与数据库交互的过程更加自然且更具可预测性。
Prisma ORM 支持两种主要工作流程:用于启动新项目和用于在现有项目中采用。对于这两种工作流程,主要的配置途径都是通过 Prisma schema。
像所有抽象一样,Prisma ORM 和其他 ORM 都隐藏了一些底层数据库细节,并带有不同的假设。
这些差异和您的用例都会影响工作流程和采用成本。希望了解它们的不同之处有助于您做出明智的决定。