面向Node.js和TypeScript的下一代 ORM工具Prisma
面向Node.js和TypeScript的下一代 ORM工具Prisma

面向Node.js和TypeScript的下一代 ORM工具Prisma

Tags
Published
Author

准备

数据库准备

数据库可以通过docker跑一个服务,但是目前市场上也有好几个能提供免费的PostgreSQL 服务云厂商,有如下几个
Supabase是一款开源的后端即服务(Backend-as-a-Service)平台,它提供了类似于Firebase的功能,包括实时数据同步、身份验证和授权,以及通过SQL查询API访问PostgreSQL数据库。Supabase免费计划可以创建两个应用,并且PostgreSQL数据库每个月提供1GB的存储空间和50GB的传输量,个人开发应该绰绰有余,这里我采用Supabase ,首先注册个账号,新建个应用
notion image
创建应用后,在设置选项中查看连接选项, 可以看到在这个数据库URI地址
notion image
Supabase本身就提供了数据库相关操作的功能,可以直接通过快捷的UI交互,创建表格和字段,执行查询语句,查看表数据等,但这里我们重点是使用它来学习 Prisma这一新一代ORM工具

初始化项目

Prisma主要由三部分组成:
  • Prisma Client: 为Node.jsTypeScript自动生成和类型安全的查询生成器
  • Prisma Migrate: 迁移工具,可以轻松地将数据库模式从原型设计应用到生产
可用于各种工具和框架, 以Nextjs使用举例,你可以决定在构建时(getStaticProps)、请求时(getServerSideProps)、使用 API 路由或将后端完全分离成独立的服务器来访问您的数据库,如下分别对应四种场景
notion image
 
notion image
notion image
notion image
 
首先通过命令创建一个Nextjs项目
npx create-next-app@latest
安装Prisma
npm install prisma
初始化prisma设置
npx prisma init
执行完后,多了个配置文件prisma/schema.prisma.env文件,内容如下
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") }
其中datasource代表数据源
provider默认就是postgresql无需修改,其根据不同的数据库还支持mysqlsqlitesqlservermongodb
url 字段指连接 URL。通常由以下部分组成(SQLite 除外):
  • User: 数据库用户名
  • Password: 数据库用户密码
  • Host: 数据库服务器的 ip 或者域名
  • Port: 数据库服务器的端口
  • Database name: 数据库名称
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
 
根据提示,我们将env中的数据库地址修改成我们的地址
notion image

Prisma Schema

Prisma schema 的数据模型定义部分定义了你的应用模型(也称为 Prisma 模型)。模型:
  • 构成了应用领域的 实体
  • 映射到数据库的 (关系型数据库,例如 PostgreSQL)或 集合 (MongoDB)
假设我们有以下模型
model User { id Int @id @default(autoincrement()) email String @unique name String? role Role @default(USER) posts Post[] profile Profile? } model Profile { id Int @id @default(autoincrement()) bio String user User @relation(fields: [userId], references: [id]) userId Int } model Post { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) title String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int categories Category[] @relation(references: [id]) } model Category { id Int @id @default(autoincrement()) name String posts Post[] @relation(references: [id]) } enum Role { USER ADMIN }
其反映的的数据库如下所示:
notion image
模型会映射到数据源的底层结构
  • PostgreSQLMySQL 等关系型数据库中,model 映射至 
  • MongoDB 中,model 映射至 集合
model  支持的全部字段标量类型可以参考文档, 值得注意的是,上面User中的postsprofile是关系字段,分别代表一个用户有多个posts和一个用户有一个关联的profile, 这另个字段属于关系字段,关系字段在 Prisma 层定义模型间的联系,且 不存在于数据库中。这些字段用于生成 Prisma Client , 如果没有这个关系字段,Prisma插件会有警告
notion image
这时候可以通过执行以下命令来自动补全
npx prisma format
 

Prisma Migrate

Prisma Migrate 是一个命令式数据库架构迁移工具,它使您能够:
  • 维护数据库中的现有数据
Prisma Migrate 会生成一个.sql迁移文件的历史记录,能在开发或和部署中发挥作用。
💡
不支持 MongoDB
Prisma Migrate 目前不支持 MongoDB connector
notion image
数据模型中的每项功能都会映射成基础数据库中的相应功能,创建Prisma模型
model User { id Int @id @default(autoincrement()) name String Post Post[] } model Post { id Int @id @default(autoincrement()) title String published Boolean @default(true) authorId Int author User @relation(fields: [authorId], references: [id]) }
创建第一个迁移:
npx prisma migrate dev --name init
执行完后目录下多了一个文件
migrations/ └─ 20230420131352_init/ └─ migration.sql
对应内容如下
-- CreateTable CREATE TABLE "User" ( "id" SERIAL NOT NULL, "name" TEXT NOT NULL, CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Post" ( "id" SERIAL NOT NULL, "title" TEXT NOT NULL, "published" BOOLEAN NOT NULL DEFAULT true, "authorId" INTEGER NOT NULL, CONSTRAINT "Post_pkey" PRIMARY KEY ("id") ); -- AddForeignKey ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
同时我们的数据库的表也被创建出来
notion image
假如我们再次新增一个性别字段gender
model User { id Int @id @default(autoincrement()) gender Int name String posts Post[] }
创建第二次迁移
npx prisma migrate dev --name add_gender
Prisma架构再次与数据库架构同步,迁移历史记录里包含了两次迁移
migrations/ └─ 20230420131352_init/ └─ migration.sql └─ 20230420132624_add_gender/ └─ migration.sql
-- AlterTable ALTER TABLE "User" ADD COLUMN "gender" INTEGER NOT NULL;
 

db push

db push  使用与 Prisma Migrate 相关的引擎来同步 Prisma 架构和数据库架构,最适合于架构原型db push 命令:
  1. 检查数据库,以推断和执行所需的更改,使数据库架构反映 Prisma 架构的状态。
  1. 默认情况下,将更改应用到数据库架构后会触发生成器 (例如,Prisma Client)。您不需要手动调用Prisma生成
  1. 如果 db push 预测到更改可能导致数据丢失,它会:
      • 抛出一个错误
      • 如果仍要进行更改,则需要加上-accept-data-loss选项
💡
请注意: db push 不与迁移交互或依赖迁移。不会更新迁移表,也不会生成迁移文件。
在开发中,我们可能也不确定需要哪些字段,字段哪个类型合适,因此会频繁修改数据库结构,会持续迭代 schema,当迭代到满足需求直到它达到一个相对稳定的状态时,这时我们才生成一个迁移文件,到达初始原型的步骤没有被保留也不需要保留, 该命令常用于数据库原型设计阶段。
加入我们在User中再添加一个头像avatar字段,并执行
npx prisma db push
这是远程数据库表结构已经更新,再次生成迁移记录时
npx prisma migrate dev --name added-avatar
会打印警告提示
notion image
因为您在原型设计期间手动进行的更改和使用 db push 进行的更改不属于迁移历史记录, 会检测到已有的迁移和数据库目前状态对应不起来。Prisma Migrate 会复制现有的迁移历史,根据 schema 变化生成一个新的迁移,并将这些变化应用到数据库。
 

Prisma Studio

Prisma Studio 是数据库中数据的可视化编辑器
npx prisma studio
执行上面命令会打开http://localhost:5555页面
notion image
 
notion image
在页面可以查看项目的所有 model并用一种很方便的交互方式来操作数据库

Prisma Client

Prisma Client 是一个自动生成的类型安全查询构造器,可根据你的数据进行定制。

生成 PrismaClient 实例

Prisma Client 是一个跟据你的数据库 schema 自动生成的定制化数据库客户端 client。默认情况下, Prisma Client 被生成到 node_modules/.prisma/client 文件夹下
  1. 安装 @prisma/clientnpm 包:
    1. npm install @prisma/client
  1. 使用以下命令生成 Prisma Client:
    1. prisma generate
 
💡
重要提示 每次对 Prisma schema 进行更改后,你都需要重新运行命令 prisma generate去更新生成的 Prisma Client 代码。
例如我们的schema
model User { id Int @id @default(autoincrement()) Post Post[] }
修改为
model User { id Int @id @default(autoincrement()) posts Post[] }
再次运行prisma generate命令后, 类型也得到更新,提示需要进行修改
notion image

创建 PrismaClient 实例

以下示例演示了如何使用 默认输出路径 导入并创建你 生成的 PrismaClient 实例:
import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient()
你的应用程序通常只应当创建 一个PrismaClient 实例。如何实现这一点取决于你是在 长期运行的应用程序 还是在 无主机 (serverless) 环境中使用 Prisma。这样做的原因是每个 PrismaClient  实例会维护一个连接池,意味着大量数据库连接会 耗尽数据库连接数限制, 太多的数据库连接可能会 使你的数据库变慢并最终导致错误,这适用于所有数据库连接器 。
在长期运行的应用程序中的PrismaClient
长期运行的应用程序中,建议:
  • ✔ 创建一个PrismaClient实例,并在整个应用程序中重复使用它
    • 默认情况下适用默认的池大小(num_physical_cpus * 2 + 1)- 你不需要设置connection_limit参数
该对象在模块第一次被导入时被缓存。随后的请求会返回缓存的对象,而不是创建一个新的PrismaClient:
import { PrismaClient } from '@prisma/client' let prisma = new PrismaClient() export default prisma
防止热重载创建PrismaClient的新实例
Next.js这样的框架支持热重载改变的文件,这使你能够在不重启的情况下看到你的应用程序的变化。然而,如果框架刷新了负责导出PrismaClient的模块,这可能会导致开发环境中出现额外的、不需要的PrismaClient实例
作为一个变通方法,你可以在开发环境中把PrismaClient作为一个全局变量来存储,因为全局变量不会被重新加载:
import { PrismaClient } from '@prisma/client' // add prisma to the NodeJS global type interface CustomNodeJsGlobal extends NodeJS.Global { prisma: PrismaClient } // Prevent multiple instances of Prisma Client in development declare const global: CustomNodeJsGlobal const prisma = global.prisma || new PrismaClient() if (process.env.NODE_ENV === 'development') global.prisma = prisma export default prisma
 

连接和断开

PrismaClient 使用以下两种方法连接和断开数据源:
在大多数情况下,你 不需要显式调用这些方法PrismaClient 会在你运行第一条查询时自动连接数据库,并创建一个 连接池,然后在 Node.js 进程结束时断开连接。

CRUD 增删改查

接下来介绍如何使用生成的 Prisma Client API 执行 CRUD 操作
所有示例均基于以下 schema:
展开示例 schema
model ExtendedProfile { id Int @id @default(autoincrement()) biography String user User @relation(fields: [userId], references: [id]) userId Int } model User { id Int @id @default(autoincrement()) name String? email String @unique profileViews Int @default(0) role Role @default(USER) coinflips Boolean[] posts Post[] profile ExtendedProfile? } model Post { id Int @id @default(autoincrement()) title String published Boolean @default(true) author User @relation(fields: [authorId], references: [id]) authorId Int comments Json? views Int @default(0) likes Int @default(0) categories Category[] } model Category { id Int @id @default(autoincrement()) name String @unique posts Post[] } enum Role { USER ADMIN }

创建

创建单个
以下查询创建(create )具有两个字段的单个用户:
const user = await prisma.user.create({ data: { email: 'elsa@prisma.io', name: 'Elsa Prisma', }, })

创建多个记录

Prisma Client 在 2.20.0 和更高版本中支持将大量插入作为 基础可用 功能。
以下 createMany  查询创建多个用户并跳过任何重复项 (email 必须是唯一的):
const createMany = await prisma.user.createMany({ data: [ { name: 'Bob', email: 'bob@prisma.io' }, { name: 'Bobo', email: 'bob@prisma.io' }, // 唯一键重复! { name: 'Yewande', email: 'yewande@prisma.io' }, { name: 'Angelique', email: 'angelique@prisma.io' }, ], skipDuplicates: true, // 跳过 'Bobo' })

读取

按 ID 或唯一标识符获取记录
以下查询按唯一标识符或 ID 返回单个记录 (findUnique )
// 按唯一标识符 const user = await prisma.user.findUnique({ where: { email: 'elsa@prisma.io', }, }) // 按 ID const user = await prisma.user.findUnique({ where: { id: 99, }, })
获取所有记录
以下 findMany  查询返回 所有 User 记录:
const users = await prisma.user.findMany()
获取与特定条件匹配的第一条记录
以下 findFirst  查询返回 至少有一个帖子有超过 100 个赞的 最新创建的用户
  1. 按升序 ID 排序用户(最大的优先)- 最大的 ID 是最近的 ID
  1. 以升序返回第一个用户,其中至少有一个帖子有 100 个以上的赞
const findUser = await prisma.user.findFirst({ where: { posts: { some: { likes: { gt: 100 } } } }, orderBy: { id: "asc" } }) }
获取经过过滤的记录列表
Prisma Client 支持记录字段和相关记录字段的 过滤
按单个字段值过滤
下面的查询返回所有 User 记录,其中包含以 "prisma.io" 结尾的电子邮件:
const users = await prisma.user.findMany({ where: { email: { endsWith: "prisma.io" } }, }
按多个字段值过滤
以下查询使用 operators  组合返回名称以 E 开头的用户  至少具有 1 个 profile 的管理员:
const users = await prisma.user.findMany({ where: { OR: [ { name: { startsWith: 'E', }, }, { AND: { profileViews: { gt: 0, }, role: { equals: 'ADMIN', }, }, }, ], }, })
按相关记录字段值过滤
以下查询返回的用户电子邮件以 prisma.io 结尾,并且 至少有  篇(some)为未发布的帖子:
const users = await prisma.user.findMany({ where: { email: { endsWith: "prisma.io" }, posts: { some: { published: false } } }, }
有关过滤相关字段值的更多示例,请参见 使用关系

选择字段

默认情况下,当查询返回记录(与计数相反)时,结果包括 默认选择集
  • Prisma schema 中定义的 所有 标量字段(包括枚举)
  •  关系
要自定义结果,请执行以下操作:
可以仅选择所需的字段和关系,而不依赖默认选择集 ✔ 减小响应的大小并 ✔ 提高查询速度。
以下示例仅返回 email 和 name 字段:

选择特定字段

使用 select 返回有限的字段子集,而不是所有字段。以下示例仅返回 email 和 name 字段:
// 返回一个对象或 null const getUser: object | null = await prisma.user.findUnique({ where: { id: 22, }, select: { email: true, name: true, }, })

包括关系并选择关系字段

要返回 特定关系字段,你可以:
  • 使用嵌套的 select -在 include 中使用 select
要返回 all 关系字段,请仅使用 include - 例如 { include: { posts: true } }。
以下查询使用嵌套的 select 来选择每个用户的 name 和每个相关帖子的 title
const users = await prisma.user.findMany({ select: { name: true, posts: { select: { title: true, }, }, }, })
Show CLI results
以下查询在 include 中使用 select,并返回 所有 用户字段和每篇文章的 title 字段:
const users = await prisma.user.findMany({ // 返回所有用户字段 include: { posts: { select: { title: true, }, }, }, })
Show CLI results
有关查询关系的更多信息,请参阅以下文档:
 

分页

Prisma Client 支持偏移分页和基于游标的分页。

偏移分页

偏移分页使用 skip 和 take 跳过一定数量的结果并选择有限的范围。以下查询跳过前 3 个 Post 记录并返回记录 4 - 7:
const results = await prisma.post.findMany({ skip: 3, take: 4, })
notion image
要实现结果页面,只需 skip 页面数乘以每页显示的结果数即可。

基于游标的分页

基于游标的分页使用 cursor and take 在给定 游标 之前或之后返回一组有限的结果。游标在结果集中为你的位置添加书签,并且必须是唯一的连续列,例如 ID 或时间戳。
以下示例返回包含单词 "Prisma" 的前 4 条 Post 记录,并将最后一条记录的 ID 保存为 myCursor
注意:由于这是第一个查询,因此没有要传递游标。
const firstQueryResults = prisma.post.findMany({ take: 4, where: { title: { contains: 'Prisma'/* 可选过滤器 */, }, }, orderBy: { id: 'asc', }, }) // 在结果集中为你的位置添加书签 - 在此 // 案例,列表 4 中最后一篇文章的 ID。 const lastPostInResults = firstQueryResults[3]// 记住:从零开始的索引!:) const myCursor = lastPostInResults.id// 示例: 29
下图显示了前 4 个结果 - 或第 1 页的 ID。下一个查询的游标为 29
notion image
第二个查询返回前 4 个 Post 记录,这些记录包含单词 "Prisma" 在提供的游标之后(换句话说,大于 29 的 ID):
const secondQueryResults = prisma.post.findMany({ take: 4, skip: 1,// 跳过游标 cursor: { id: myCursor, }, where: { title: { contains: 'Prisma'/* 可选过滤器 */, }, }, orderBy: { id: 'asc', }, }) const lastPostInResults = secondQueryResults[3]// 记住:从零开始的索引!:) const myCursor = lastPostInResults.id// 示例: 52
下图显示了 ID 为 29 的记录 之后 的前 4 条 Post 记录。在本例中,新游标为 52
notion image

嵌套写入

嵌套写入 允许你使用多个 操作 执行单个 Prisma Client API 调用,这些操作涉及多个 相关的 记录。例如,创建 用户 和 post 或更新 订单 和 发票Prisma Client 确保所有操作作为一个整体成功或失败。
下面的示例演示了带有 create 的嵌套写入:
// 在一个事务中创建一个具有两个帖子的新用户 const newUser: User = await prisma.user.create({ data: { email: 'alice@prisma.io', posts: { create: [ { title: 'Join the Prisma Slack on https://slack.prisma.io' }, { title: 'Follow @prisma on Twitter' }, ], }, }, })
以下示例演示了带有 update 的嵌套写入:
// 在单个事务中更改帖子的作者 const updatedPost: Post = await prisma.post.update({ where: { id: 42 }, data: { author: { connect: { email: 'alice@prisma.io' }, }, }, })

内省

将 Prisma 添加到现有项目时,内省通常用于生成数据模型的 初始 版本。

内省能做什么?

内省有一个主要功能: 反映当前数据库架构的数据模型来填充您的 Prisma 架构。
notion image
以下是它的主要功能概述:
  • 将数据库中的  映射到 Prisma 模型的字段
  • 将数据库中的 indexes 映射到 Prisma 架构中的indexes

prisma db pull 命令

您可以使用 Prisma CLI 的 prisma db pull 命令内省您的数据库。请注意,使用此命令需要在您的 Prisma 架构 datource中设置连接 URL

内省的工作流

不使用 Prisma Migrate,而是使用纯 SQL 或其他迁移工具的项目的典型工作流如下:
  1. 更改数据库架构 (例如使用纯 SQL)
  1. 运行 prisma db pull 更新 Prisma 架构
  1. 运行 prisma generate Prisma Client
  1. 在应用程序中使用更新的 Prisma Client
请注意,随着应用程序的发展,此过程可以重复无限次。
notion image
 
我们可以执行以下命令来生成您的 Prisma 模型:
npx prisma db pull
创建了以下 Prisma 模式:
model category { id Int @id @default(autoincrement()) name String post_categories_category post_categories_category[] } model profile { id Int @id @default(autoincrement()) bio String? userId Int? @unique user user? @relation(fields: [userId], references: [id]) } model user { id Int @id @default(autoincrement()) name String? email String @unique post post[] profile profile? }
 

调整 Prisma schema(可选)

所有调整都是可选的,不想调整任何内容,可以跳到下一步。 后续随时返回调整。
Prisma 的命名与当前 snake_case 的命名约定不同:
  • 模型(Prisma model)命名遵守 PascalCase
  • 字段(field)命名遵守 camelCase
可以使用 @@map 和 @map 映射的模型和字段名称到已有的数据表和列名来调整命名。
另请注意,可以重命名 关联字段 以优化稍后将用于向数据库发送查询的 Prisma Client API。 例如,user 模型上的 post 字段是数组格式,因此这个字段的更好名称是 posts 以表明它是复数形式。
model Category { id Int @id @default(autoincrement()) name String postsToCategories PostToCategories[] @@map("category") } model Profile { id Int @id @default(autoincrement()) bio String? userId Int? @unique user User? @relation(fields: [userId], references: [id]) @@map("profile") } model User { id Int @id @default(autoincrement()) name String? email String @unique posts Post[] profile Profile? @@map("user") }

总结

最后我们以问答的形式来总结一下prisma
Prisma是什么?它的作用是什么?
Prisma是一款现代化的ORM框架,它可以连接到多种数据库类型(如PostgreSQLMySQLSQLiteSQL Server),提供高效、类型安全和易于使用的API,从而方便地操作和管理数据库。它的作用是帮助开发人员快速构建可扩展、高性能的数据库应用程序,同时减少手写底层SQL代码的工作量。
Prisma有哪些主要的特性和功能?
Prisma的主要特性和功能包括:
  • 数据建模:支持通过编写数据模型(schema)定义应用程序的数据结构来定义数据库中的表、字段、关系、索引等内容。
  • 数据查询:提供基于类型安全的查询API来查询数据库,并支持AND/OR逻辑运算、分页、过滤、排序、连接查询等高级功能。
  • 数据修改:支持通过数据编辑API来添加、更新和删除数据库中的数据,并提供事务管理等功能。
  • 数据迁移:支持数据库模式的迁移操作,方便在开发、测试和生产环境之间进行数据库的版本控制和管理。
Prisma如何处理关系型数据?
Prisma的关系型数据库支持包括一对一、一对多和多对多关系。Prisma的数据建模语言允许开发人员定义表之间的关系,并在查询时自动处理关系查询、连接和过滤。
Prisma基于外键关系自动管理关系的连接和解除连接。在定义数据模型时,可以使用关系属性来建立连接。例如,下面的代码定义了一个名为posts的文档集合:
model Post { id Int @id @default(autoincrement()) title String body String comments Comment[] } model Comment { id Int @id @default(autoincrement()) body String postId Int post Post @relation(fields: [postId], references: [id]) }
在这个例子中,Comment模型具有一个指向Post模型的引用,并且在执行查询时,Prisma会自动处理两个模型之间的连接。
 
 
Prisma 与传统TypeORM 有什么区别和优势?
虽然PrismaTypeORM解决了类似的问题,但它们的工作方式非常不同。
TypeORM是一种传统的ORM,将表映射到模型类。这些模型类可用于生成SQL迁移。然后,模型类的实例在运行时为应用程序提供CRUD查询接口。
Prisma是一种新类型的ORM,可以缓解传统ORM中许多问题,例如臃肿的模型实例、业务逻辑与存储逻辑混合、缺乏类型安全性或由懒加载引起的不可预测查询等。
TypeORM为其查找方法(例如findfindByIdsfindOne等)提供了一个select选项,例如:
const postRepository = getManager().getRepository(Post) const publishedPosts: Post[] = await postRepository.find({ where: { published: true }, select: ['id', 'title'], })
虽然返回的publishedPosts数组中的每个对象在运行时只携带所选的idtitle属性,但TypeScript编译器并不知道这一点。它将允许您在查询之后访问Post实体上定义的任何其他属性,例如:
const post = publishedPosts[0] // TypeScript编译器没有问题 if (post.content.length > 0) { console.log(`This post has some content.`) }
此代码将导致运行时错误:
TypeError: Cannot read property 'length' of undefined
Prisma Client可以在相同情况下保证完全类型安全,并防止您访问未从数据库检索到的字段。考虑使用Prisma Client查询相同示例,在这种情况下,TypeScript编译器将在编译时抛出以下错误:
[ERROR] 14:03:39 ⨯ Unable to compile TypeScript: src/index.ts:36:12 - error TS2339: Property 'content' does not exist on type '{ id: number; title: string; }'. 42 if (post.content.length > 0) {
这是因为Prisma Client动态生成其查询的返回类型。在本例中,publishedPosts的类型如下所示:
const publishedPosts: { id: number title: string }[]
因此,您无法访问未有类型声明的属性