四、Prisma操作数据库

PrismaNodejs, TypeScript 的下一代数据库 ORM

由以下几部分组成:

  • Prisma Client - 类型安全的查询生成器
  • Prisma Migrate - 迁移工具
  • Prisma Studio - 数据库GUI

我们本章节不会涉及到过多的内容 , 专注于以下几点:

  • Schema - 数据库的表结构定义
  • Client - 客户端工具 CRUD 的基本用法
  • Migrate - 数据迁移的操作方法

安装prisma

  1. 安装依赖:
1
pnpm add ts-node prisma -D
  1. 初始化prisma
1
npx prisma init

此命令做了两件事:

  • 在根目录下创建一个名为prisma的文件夹且其中包含一个文件schema.prisma
  • 在根目录下创建.env文件
  1. 连接数据库

默认创建的schema使用的是PostgreSQL,我们后续以MySQL做演示

schema.prisma中指定数据库:

1
2
3
4
5
// prisma/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

.env中配置数据库链接:

1
2
// .env
DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/DATABASE"

快速上手

  1. 定义schema

prisma/schema.prisma中添加数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}

model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
}
  1. 数据模型映射到数据库架构
1
npx prisma migrate dev --name init

此命令执行两项操作:

  • 为这次迁移创建了一个新的SQL迁移文件

  • 对数据库运行SQL迁移文件

注意:默认情况下,在运行prisma migrate dev之后,会调用generategenerate会根据schema生成TypeScript类型声明)。如果你的模式中定义了prisma-client-js生成器,这将检查@prisma/client是否已经安装,如果它没有安装,就安装它。

  1. 安装prisma client
1
pnpm add @prisma/client

安装命令会自动调用prisma generate,它会读取prisma模式并生成一个适合您的模型的prisma Client版本。

以后每当您对 Prisma 架构进行更改时,您都需要手动调用prisma generate以适应 Prisma 客户端 API 中的更改。

  1. 操作数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
// ... you will write your Prisma Client queries here
await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@prisma.io',
posts: {
create: { title: 'Hello World' },
},
profile: {
create: { bio: 'I like turtles' },
},
},
})

const allUsers = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
})
console.dir(allUsers, { depth: null })
}

main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})

这里只是做了个快速上手demo,接下来我们会演示在Nextjs中使用prisma的方式

Nextjs使用prisma

前边做了快速上手示例,但是在Nextjs中直接这么使用是不合适的,如果按照上边的方式,在使用nextjs开发中,next dev 会在运行时清除 Node.js 缓存。这反过来又会初始化一个新的 PrismaClient 实例,因为热重载会创建与数据库的连接。这会快速耗尽数据库连接,因为每个 PrismaClient 实例都拥有自己的连接池。

在这种情况下,解决方案是实例化单个实例 PrismaClient 并将其保存在globalThis对象。然后,我们检查仅实例化 PrismaClient(如果它不在 globalThis 对象上),否则如果已经存在,请再次使用相同的实例,以防止实例化额外的 PrismaClient 实例。

我们在db/prisma.ts中对其进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// db/prisma.ts
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
return new PrismaClient()
}

declare global {
var prisma: undefined | ReturnType<typeof prismaClientSingleton>
}

const prisma = globalThis.prisma ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma

只有开发环境才有这个问题,所以我们执行了if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma保证开发环境下把prisma保存到全局对象,避免重复创建。生产环境下不会有热重载等情况,也就不存在这个问题了。

之后,我们在需要操作数据库的地方,就可以导入这个封装后的prisma来使用了。

Schema

Prisma Schema文件是核心配置文件,命名为prisma.schema,包含三个部分:

  • datasource - 配置数据库
  • generator - 目前仅支持provider=prisma-client-js
  • model - 实体

Model 定义:

  • 表示应用的entity实体
  • 映射成数据库的table(在mongo中映射为collection
  • prisma client api使用的基础
  • 如果使用typescript,会为model生成类型定义。

有以下部分组成:

  • 包含多个字段(包括模型关系)
  • 枚举
  • 更改字段和模型行为的属性和函数

模型与表的映射

prisma模型命名约定为单数形式,大驼峰,而数据库中的命名方式一般是复数形式,下划线分隔

所以我们一般需要进行表名或字段名的映射

可以使用@@map来自定义命名表名。用@map来自定义字段名

1
2
3
4
5
6
// prisma/prisma.schema
model Comment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now) @map("created_at")
@@map("comments")
}

在客户端进行使用的时候会使用小写形式,比如模型Commentclient.comment,模型CommentPage则使用client.commentPage这样子。字段名还是以模型中定义的形式来使用,@map只是对于数据库中表字段的映射

定义字段

字段由以下几部分组成:

  • 字段名称
  • 字段类型
  • 可选的类型修饰符
  • 可选的属性

可以通过以下两个修饰符来修改字段的类型:

  • [] - 将字段设为列表
  • ? - 将字段设为可选

不能组合类型修饰符,即不支持可选的列表

定义ID

ID 唯一标识模型的各个记录。一个模型只能有一个 ID

  • 关系数据库中ID 可以是单个字段,也可以基于多个字段。如果模型没有@id@@id,则必须改为定义必填@unique字段或@@unique块。
  • MongoDB中,ID必须是定义@id属性和@map("_id")属性的单个字段。

在关系型数据库定义ID

  1. 单字段ID
1
2
3
4
5
6
7
8
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
}
  1. 复合ID
1
2
3
4
5
6
7
8
model User {
firstName String
lastName String
email String @unique
isAdmin Boolean @default(false)

@@id([firstName, lastName])
}

默认情况下,Prisma 客户端查询中此字段的名称将firstName_lastName.

您还可以使用@@id属性name复合 ID 提供自己的名称:

1
2
3
4
5
6
7
8
model User {
firstName String
lastName String
email String @unique
isAdmin Boolean @default(false)

@@id(name: "fullName", fields: [firstName, lastName])
}

firstName_lastName字段现在将改为命名为 fullName

定义字段属性

属性修改字段或模型模块的行为。以下示例包括三个字段属性(@id@default@unique )和一个块属性 (@@unique ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 关系型数据库:
model User {
id Int @id @default(autoincrement())
firstName String
lastName String
email String @unique
isAdmin Boolean @default(false)

@@unique([firstName, lastName])
}

# mongoDB:
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
firstName String
lastName String
email String @unique
isAdmin Boolean @default(false)

@@unique([firstName, lastName])
}

常见字段属性:

通过@default定义默认值,可以使用常量值和prisma提供的函数。

通过@unqiue实现唯一值。

通过模型上的@@index在模型的一个或多个字段上定义索引

枚举

如果数据库连接器支持,则可以在数据模型中定义枚举

1
2
3
4
5
6
7
8
9
10
11
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
}

enum Role {
USER
ADMIN
}

CRUD

常用的CRUD方法:

  • findMany
  • findFirst
  • findFirstOrThrow
  • findUnique
  • findUniqueOrThrow
  • create
  • update
  • upsert
  • delete
  • createMany
  • updateMany
  • deleteMany

这些操作可通过 Prisma 客户端实例上生成的属性进行访问。默认情况下,属性的名称是模型名称的小写形式,例如,用户User模型的 userPost 模型的 post。使用时比如await prisma.user.findFirst(...)

更改schema后记得执行generate命令,才能获得TypeScript类型提示哦

Migrate

我们经常会用到的命令有migratedb push

  1. Prisma Migrate
    • Prisma Migrate 是一个迁移工具,可以轻松地将数据库模式从原型设计应用到生产。
    • 它主要用于处理数据库模式的变更,包括添加、修改或删除表、索引和其他数据库对象。
    • 通过 Prisma Migrate,你可以在 schema.prisma 文件中定义模型,并将其迁移到数据库中。如果存在冲突,它将根据 schema 中的定义进行重置。
    • 你还可以使用 Prisma Migrate 进行开发环境的迁移,将更改部署到暂存、测试和生产环境。它只运行迁移文件,你可以选择只生成迁移文件而不自动执行,以便进行人工修改。
  2. Prisma DB Push
    • Prisma DB Push 是一个命令行工具,用于推送数据库结构变更。
    • 该命令可以删除并重新创建数据库,或者通过删除所有数据、表、索引和其他来进行重置。
    • 在某些情况下,你可能需要完全重置数据库并重新创建其结构。这时,你可以使用 Prisma DB Push 来实现这一目标。

总结:

  • Prisma Migrate 主要用于处理数据库模式的变更,可以根据需要进行迁移和开发环境的部署。
  • Prisma DB Push 则用于完全重置和重新创建数据库。

在使用时,你可以根据具体需求选择合适的工具。如果你需要处理数据库模式的变更并部署到不同环境,可以使用 Prisma Migrate。而如果你需要完全重置数据库,可以使用 Prisma DB Push

prisma migrate不适用于mongo,请在mongo中使用db push

实例

目前为止,我们已经接触到了Nextjs, ShadcnUI, React Query, Prisma,在下一个章节, 我们基于这些知识做一个小的案例来试一下。

作者

胡兆磊

发布于

2024-06-23

更新于

2024-06-24

许可协议