Prisma ORM 是 ORM 吗?
简要回答这个问题:是的,Prisma ORM 是一种新型 ORM,它从根本上不同于传统的 ORM,并且不会遇到与这些传统 ORM 相关的许多问题。
传统 ORM 提供了一种面向对象的方式来处理关系数据库,通过将表映射到您编程语言中的模型类。 这种方法导致许多问题,这些问题是由对象关系阻抗失配引起的。
Prisma ORM 的工作方式与之根本不同。 使用 Prisma ORM,您可以在声明式的 Prisma schema 中定义您的模型,Prisma 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
Data Mapper ORM 与 Active Record 相比,将应用程序的内存数据表示形式与数据库的表示形式解耦。 这种解耦是通过要求您将映射责任分离为两种类型的类来实现的
- 实体类:应用程序中实体的内存表示形式,它们不知道数据库的存在
- 映射器类:这些类有两个职责
- 在两种表示形式之间转换数据。
- 生成从数据库获取数据并在数据库中持久化更改所需的 SQL。
Data Mapper ORM 允许在代码中实现的问题域和数据库之间具有更大的灵活性。 这是因为数据映射器模式允许您隐藏数据库的实现方式,但这并不是在整个数据映射层之后思考您的域的理想方式。
传统的 Data Mapper 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,您可以形成数据库表的对象图表示,这可能会导致对象关系阻抗失配。 当您正在解决的问题形成一个复杂的对象图,而该对象图不能简单地映射到关系数据库时,就会发生这种情况。 在关系数据库中的一种数据表示形式和内存中(使用对象)的另一种数据表示形式之间进行同步非常困难。 这是因为与关系数据库记录相比,对象在它们彼此关联的方式上更加灵活和多样。
- 虽然 ORM 处理了与问题相关的复杂性,但同步问题并没有消失。 对数据库 schema 或数据模型的任何更改都需要将更改映射回另一侧。 这种负担通常落在开发人员身上。 在团队进行项目的上下文中,数据库 schema 更改需要协调。
- 由于 ORM 封装的复杂性,ORM 往往具有很大的 API 表面。 不必编写 SQL 的另一面是,您花费大量时间学习如何使用 ORM。 这适用于大多数抽象,但是如果不了解数据库的工作原理,改进慢查询可能会很困难。
- 由于 SQL 提供的灵活性,某些复杂查询不受 ORM 支持。 原始 SQL 查询功能缓解了这个问题,您可以在其中将 SQL 语句字符串传递给 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 | 数据库中数据的结构,例如,表和列 | 手动编写或使用程序化 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 Migrate 来使用 Prisma Client。 例如,一个新项目可以选择使用 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: '[email protected]',
},
})
在此查询中,findUnique()
方法用于从 User
表中获取单行。 默认情况下,Prisma ORM 将返回 User
表中的所有标量字段。
注意:该示例使用 TypeScript 以充分利用 Prisma Client 提供的类型安全功能。 但是,Prisma ORM 也适用于 Node.js 中的 JavaScript。
Prisma Client 通过从 Prisma schema 生成代码,将查询和结果映射到结构类型。 这意味着 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
的类型也将包含 Post
s,可以通过 posts
数组字段访问
console.log(user.posts[0].title)
此示例仅触及 Prisma Client API 的冰山一角,关于 CRUD 操作,您可以在文档中了解更多信息。主要思想是所有查询和结果都由类型支持,并且您可以完全控制关联关系的获取方式。
结论
总而言之,Prisma ORM 是一种新型的数据映射器 ORM,它不同于传统的 ORM,并且没有传统 ORM 常见的那些问题。
与传统 ORM 不同,使用 Prisma ORM,您可以定义 Prisma schema – 数据库模式和应用程序模型的声明式单一事实来源。Prisma Client 中的所有查询都返回纯 JavaScript 对象,这使得与数据库交互的过程更加自然和可预测。
Prisma ORM 支持两种主要的工作流程,用于启动新项目和在现有项目中采用。对于这两种工作流程,您的主要配置途径是通过 Prisma schema。
像所有抽象一样,Prisma ORM 和其他 ORM 都以不同的假设隐藏了数据库的一些底层细节。
这些差异和您的用例都会影响采用的工作流程和成本。希望了解它们之间的差异可以帮助您做出明智的决定。