跳至主要内容

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

user-table

这是相应的实体类的样子

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 语句(如ActiveRecordSQLAlchemy)。

因为数据库通常包含数据,所以迁移有助于将架构更改分解成更小的单元,这有助于避免意外的数据丢失。

假设您是从头开始一个项目,以下是一个完整的工作流程:您创建一个迁移,该迁移将在数据库架构中创建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 有助于减少代码量。它们可以免去你为常见的 CRUD(创建、读取、更新、删除)操作编写重复的 SQL 语句,并对用户输入进行转义以防止 SQL 注入等漏洞。
  • ORM 要求你编写很少甚至不编写 SQL(取决于你的复杂性,你可能仍然需要编写一些原始查询)。这对不熟悉 SQL 但仍想使用数据库的开发人员来说是有益的。
  • 许多 ORM 隐藏了数据库特定的细节。理论上,这意味着 ORM 可以使从一个数据库更改到另一个数据库变得更容易。需要注意的是,在实践中,应用程序很少更改其使用的数据库。

与所有旨在提高生产力的抽象一样,使用 ORM 也存在一些缺点。

ORM 的缺点

ORM 的缺点在你开始使用它们时并不总是显而易见的。本节涵盖了一些普遍接受的缺点

  • 使用 ORM,你会形成数据库表的“对象图”表示,这可能导致 对象关系阻抗不匹配。当你要解决的问题形成一个复杂的“对象图”时,它不能简单地映射到关系型数据库。同步两种不同数据表示,一种在关系型数据库中,另一种在内存中(使用对象),是相当困难的。这是因为与关系型数据库记录相比,对象在相互关联的方式上更加灵活和多样化。
  • 虽然 ORM 处理了与该问题相关的复杂性,但同步问题并没有消失。对数据库模式或数据模型的任何更改都需要将更改映射回另一侧。这项任务通常由开发人员承担。在一个团队共同完成项目的情况下,数据库模式更改需要协调。
  • ORM 由于其封装的复杂性,往往拥有庞大的 API 表面。使用 ORM 不用编写 SQL 的反面是你需要花费大量时间学习如何使用它。这适用于大多数抽象,但是如果不了解数据库的工作原理,则很难改进缓慢的查询。
  • 某些复杂查询由于 SQL 提供的灵活性而无法得到 ORM 的支持。这个问题可以通过 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 如何实现 Data Mapper ORM 模式。

Prisma ORM 如何实现 Data Mapper 模式

如本文前面所述,Data Mapper 模式与数据库和应用程序由不同团队管理的组织非常吻合。

随着现代云环境的兴起(包括托管数据库服务和 DevOps 实践),越来越多的团队采用跨功能方法,其中团队拥有整个开发周期,包括数据库和操作问题。

Prisma ORM 允许 DB 模式和对象模式同步演化,从而减少了最初出现偏差的可能性,同时仍然允许你使用 @map 属性将应用程序和数据库稍微解耦。虽然这看起来像是一个限制,但它可以防止领域模型的演化(通过对象模式)被视为事后才考虑的因素强加于数据库。

要了解 Prisma ORM 实现 Data Mapper 模式在概念上与传统 Data Mapper ORM 的区别,这里简要比较了它们的概念和构建块

概念描述传统 ORM 中的构建块Prisma ORM 中的构建块Prisma ORM 中的事实来源
对象模式应用程序中的内存中数据结构模型类生成的 TypeScript 类型Prisma 模式中的模型
数据映射器将对象模式和数据库进行转换的代码映射器类Prisma Client 中的生成函数Prisma 模式中的 @map 属性
数据库模式数据库中数据的结构,例如表和列手动编写的 SQL 或使用程序化 API 编写的 SQL由 Prisma Migrate 生成的 SQLPrisma 模式

Prisma ORM 与 Data Mapper 模式相一致,并具有以下额外优势

  • 通过根据 Prisma 模式生成 Prisma Client,减少了定义类和映射逻辑的样板代码。
  • 消除了应用程序对象与数据库模式之间的同步挑战。
  • 数据库迁移是一级公民,因为它们是从 Prisma 模式中派生的。

现在我们已经讨论了 Prisma ORM 实现 Data Mapper 模式背后的概念,我们可以了解 Prisma 模式在实践中如何工作。

Prisma 模式

Prisma 实现 Data Mapper 模式的核心是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。
  • PostUser 模型映射到数据库表。
  • 这两个模型具有1-n 关系,其中每个 User 可以拥有许多相关的 Post
  • 模型中的每个字段都有一个类型,例如 id 的类型为 Int
  • 字段可能包含字段属性以定义
    • 使用 @id 属性定义主键。
    • 使用 @unique 属性定义唯一键。
    • 使用 @default 属性定义默认值。
    • 使用 @map 属性定义表列与 Prisma Client 字段之间的映射,例如 content 字段(将在 Prisma Client 中访问)映射到 post_content 数据库列。

可以使用以下图表来可视化 User / Post 关系

1-n relation between User and Post

在 Prisma ORM 级别,User / Post 关系由以下部分组成

  • authorId 标量字段,它由 @relation 属性引用。此字段存在于数据库表中——它是连接 Post 和 User 的外键。
  • 两个关系字段:authorposts不存在于数据库表中。关系字段在 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兼容。如果您希望逐步评估和采用,则可以使用此方法作为并行采用策略的一部分。

与此工作流程兼容的非详尽设置列表

  • 使用纯SQL文件(包含CREATE TABLEALTER TABLE)来创建和更改数据库模式的项目。
  • 使用第三方迁移库(如db-migrateUmzug)的项目。
  • 已经使用ORM的项目。在这种情况下,通过ORM进行的数据库访问保持不变,而生成的Prisma Client可以逐步采用。

在实践中,这是内省现有数据库并生成Prisma Client所需的步骤

  1. 创建一个schema.prisma,定义datasource(在本例中为您的现有数据库)和generator
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}

generator client {
provider = "prisma-client-js"
}
  1. 运行prisma db pull,用从数据库模式派生的模型填充Prisma模式。
  2. (可选)自定义字段和模型映射,在Prisma Client和数据库之间。
  3. 运行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

  1. 创建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[]
}
  1. 运行prisma migrate以生成迁移的SQL,将其应用于数据库,并生成Prisma Client。

对于对数据库模式的任何进一步更改

  1. 将更改应用于Prisma模式,例如,在User模型中添加registrationDate字段
  2. 再次运行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背后的概念,它对Data Mapper模式的实现,以及它支持的工作流程。在本节的最后,您将看到如何使用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,可以通过posts数组字段访问它们

console.log(user.posts[0].title)

此示例仅介绍了Prisma Client API用于CRUD操作的内容,您可以在文档中了解更多信息。主要思想是所有查询和结果都由类型支持,您可以完全控制如何获取关系。

结论

总之,Prisma ORM是一种新型的Data Mapper ORM,它不同于传统的ORM,而且不会遭受与传统ORM相关的常见问题。

与传统的ORM不同,在Prisma ORM中,您定义Prisma模式——数据库模式和应用程序模型的声明性单一事实来源。Prisma Client中的所有查询都返回普通的JavaScript对象,这使得与数据库交互的过程更加自然,也更加可预测。

Prisma ORM支持两种主要工作流程来启动新项目并在现有项目中采用。对于这两种工作流程,您的主要配置途径都是通过Prisma模式。

像所有抽象一样,Prisma ORM和其他ORM都隐藏了数据库的一些底层细节,并使用了不同的假设。

这些差异以及您的用例都会影响采用流程和成本。希望了解它们之间的区别能够帮助您做出明智的决定。