准备
数据库准备
数据库可以通过
docker
跑一个服务,但是目前市场上也有好几个能提供免费的PostgreSQL
服务云厂商,有如下几个Supabase
是一款开源的后端即服务(Backend-as-a-Service
)平台,它提供了类似于Firebase
的功能,包括实时数据同步、身份验证和授权,以及通过SQL
查询API
访问PostgreSQL
数据库。Supabase
免费计划可以创建两个应用,并且PostgreSQL
数据库每个月提供1GB
的存储空间和50GB
的传输量,个人开发应该绰绰有余,这里我采用Supabase
,首先注册个账号,新建个应用创建应用后,在设置选项中查看连接选项, 可以看到在这个数据库
URI
地址Supabase
本身就提供了数据库相关操作的功能,可以直接通过快捷的UI
交互,创建表格和字段,执行查询语句,查看表数据等,但这里我们重点是使用它来学习 Prisma
这一新一代ORM
工具初始化项目
Prisma
主要由三部分组成:- Prisma Client: 为
Node.js
和TypeScript
自动生成和类型安全的查询生成器
- Prisma Migrate: 迁移工具,可以轻松地将数据库模式从原型设计应用到生产
- Prisma Studio: 用于查看和编辑数据库中数据的
GUI
可用于各种工具和框架, 以
Nextjs
使用举例,你可以决定在构建时(getStaticProps
)、请求时(getServerSideProps
)、使用 API
路由或将后端完全分离成独立的服务器来访问您的数据库,如下分别对应四种场景首先通过命令创建一个
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
无需修改,其根据不同的数据库还支持mysql
、sqlite
、sqlserver
、mongodb
等url
字段指连接 URL
。通常由以下部分组成(SQLite
除外):User
: 数据库用户名
Password
: 数据库用户密码
Host
: 数据库服务器的 ip 或者域名
Port
: 数据库服务器的端口
Database name
: 数据库名称
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
根据提示,我们将
env
中的数据库地址修改成我们的地址Prisma Schema
Prisma schema 的数据模型定义部分定义了你的应用模型(也称为 Prisma 模型)。模型:
- 构成了应用领域的 实体
- 映射到数据库的 表(关系型数据库,例如 PostgreSQL)或 集合 (MongoDB)
- 构成 Prisma Client API 中 查询 的基础
假设我们有以下模型
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 }
其反映的的数据库如下所示:
模型会映射到数据源的底层结构
- 在
PostgreSQL
和MySQL
等关系型数据库中,model
映射至 表
MongoDB
中,model
映射至 集合
model
支持的全部字段标量类型可以参考文档, 值得注意的是,上面User
中的posts
和profile
是关系字段,分别代表一个用户有多个posts
和一个用户有一个关联的profile
, 这另个字段属于关系字段,关系字段在 Prisma
层定义模型间的联系,且 不存在于数据库中。这些字段用于生成 Prisma Client
, 如果没有这个关系字段,Prisma插件会有警告这时候可以通过执行以下命令来自动补全
npx prisma format
Prisma Migrate
Prisma Migrate
是一个命令式数据库架构迁移工具,它使您能够:- 保持数据库架构与Prisma 架构的同步
- 维护数据库中的现有数据
不支持 MongoDB
Prisma Migrate 目前不支持 MongoDB connector
数据模型中的每项功能都会映射成基础数据库中的相应功能,创建
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;
同时我们的数据库的表也被创建出来
假如我们再次新增一个性别字段
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
- 检查数据库,以推断和执行所需的更改,使数据库架构反映
Prisma
架构的状态。
- 默认情况下,将更改应用到数据库架构后会触发生成器 (例如,
Prisma Client
)。您不需要手动调用Prisma生成
。
- 如果
db push
预测到更改可能导致数据丢失,它会: - 抛出一个错误
- 如果仍要进行更改,则需要加上
-accept-data-loss
选项
请注意:
db push
不与迁移交互或依赖迁移。不会更新迁移表,也不会生成迁移文件。在开发中,我们可能也不确定需要哪些字段,字段哪个类型合适,因此会频繁修改数据库结构,会持续迭代
schema
,当迭代到满足需求直到它达到一个相对稳定的状态时,这时我们才生成一个迁移文件,到达初始原型的步骤没有被保留也不需要保留, 该命令常用于数据库原型设计阶段。加入我们在
User
中再添加一个头像avatar
字段,并执行npx prisma db push
这是远程数据库表结构已经更新,再次生成迁移记录时
npx prisma migrate dev --name added-avatar
会打印警告提示
因为您在原型设计期间手动进行的更改和使用
db push
进行的更改不属于迁移历史记录, 会检测到已有的迁移和数据库目前状态对应不起来。Prisma Migrate
会复制现有的迁移历史,根据 schema
变化生成一个新的迁移,并将这些变化应用到数据库。Prisma Studio
Prisma Studio
是数据库中数据的可视化编辑器npx prisma studio
执行上面命令会打开http://localhost:5555页面
在页面可以查看项目的所有
model
并用一种很方便的交互方式来操作数据库Prisma Client
Prisma Client
是一个自动生成的类型安全查询构造器,可根据你的数据进行定制。生成 PrismaClient
实例
Prisma Client
是一个跟据你的数据库 schema
自动生成的定制化数据库客户端 client
。默认情况下, Prisma Client
被生成到 node_modules/.prisma/client
文件夹下- 安装
@prisma/client
npm 包:
npm install @prisma/client
- 使用以下命令生成 Prisma Client:
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
命令后, 类型也得到更新,提示需要进行修改创建 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
分配给一个全局变量仅在开发环境中以防止因创建新实例而产生热重载
该对象在模块第一次被导入时被缓存。随后的请求会返回缓存的对象,而不是创建一个新的
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
使用以下两种方法连接和断开数据源: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 个赞的 最新创建的用户:- 按升序 ID 排序用户(最大的优先)- 最大的 ID 是最近的 ID
- 以升序返回第一个用户,其中至少有一个帖子有 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, })
要实现结果页面,只需
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:
第二个查询返回前 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:嵌套写入
嵌套写入 允许你使用多个 操作 执行单个
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 架构。
以下是它的主要功能概述:
- 将数据库中的 表 映射到Prisma models
- 将数据库中的 列 映射到 Prisma 模型的字段
- 将数据库中的 indexes 映射到 Prisma 架构中的indexes
- 将 database constraints 映射到 Prisma 架构中的attributes或type modifiers
prisma db pull
命令
内省的工作流
不使用
Prisma Migrate
,而是使用纯 SQL
或其他迁移工具的项目的典型工作流如下:- 更改数据库架构 (例如使用纯
SQL
)
- 运行
prisma db pull
更新Prisma
架构
- 运行
prisma generate
Prisma Client
- 在应用程序中使用更新的
Prisma Client
请注意,随着应用程序的发展,此过程可以重复无限次。
我们可以执行以下命令来生成您的
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框架,它可以连接到多种数据库类型(如PostgreSQL
、MySQL
、SQLite
和SQL 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
有什么区别和优势?
虽然
Prisma
和TypeORM
解决了类似的问题,但它们的工作方式非常不同。TypeORM
是一种传统的ORM
,将表映射到模型类。这些模型类可用于生成SQL迁移。然后,模型类的实例在运行时为应用程序提供CRUD
查询接口。Prisma
是一种新类型的ORM
,可以缓解传统ORM
中许多问题,例如臃肿的模型实例、业务逻辑与存储逻辑混合、缺乏类型安全性或由懒加载引起的不可预测查询等。TypeORM
为其查找方法(例如find
、findByIds
、findOne
等)提供了一个select
选项,例如:const postRepository = getManager().getRepository(Post) const publishedPosts: Post[] = await postRepository.find({ where: { published: true }, select: ['id', 'title'], })
虽然返回的
publishedPosts
数组中的每个对象在运行时只携带所选的id
和title
属性,但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 }[]
因此,您无法访问未有类型声明的属性