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