# Next.js 使用 Prisma

上篇文章 Next.js 使用 Drizzle-ORM 中,我们学习使用了 Drizzle ORM (opens new window) 库,Prisma (opens new window)Drizzle ORM (opens new window) 一样都是下一代 Node.js ORM,都提供类型安全的数据库访问、迁移和可视化数据编辑等功能。

Prisma 当前版本是 V7.8.0

# Prisma VS Drizzle-ORM

Prisma 和 Drizzle ORM 都是面向 TypeScript 的数据库工具,都支持类型安全查询和数据库迁移。简单来说,Drizzle 更像一个接近 SQL 的 Query Builder,适合喜欢直接控制 SQL 的开发者;Prisma 更像一套完整的 ORM 工具链,适合希望用统一 schema、自动类型生成和更高层 API 来管理数据库的项目。

对比项 Prisma Drizzle ORM
定位 更完整的 ORM 和数据工具链 更接近 SQL 的 Query Builder
Schema 定义 使用 schema.prisma 集中描述模型 使用 TypeScript 函数描述表结构
查询风格 prisma.user.findMany() 这类对象化 API select().from().where() 这类 SQL 风格 API
类型安全 根据 schema 生成完整类型 查询结果类型友好,但更依赖开发者写对 SQL 逻辑
关系查询 支持 includeselect、nested writes、relation filters 支持关系查询,但复杂关系更接近手写 SQL 思路
迁移 Prisma Migrate 根据 schema 生成 migration Drizzle Kit 根据 TS schema 生成 migration
上手门槛 不需要很深 SQL 基础 更适合熟悉 SQL 的开发者
适合场景 团队项目、复杂模型、长期维护 小型项目、偏 SQL、希望保持轻量

如果项目里数据库关系比较复杂、多人协作较多,Prisma 会更省心;如果项目更强调轻量、SQL 可控性,Drizzle ORM 会更直接。

# 初始化项目

使用 Prisma 非常简单,下面是使用 Prisma 的具体步骤:

  1. 安装依赖
$ npm install prisma @types/pg --save-dev
$ npm install @prisma/client @prisma/adapter-pg pg dotenv
1
2

每个软件包的功能:

  • prisma - Prisma CLI,用于运行诸如 prisma initprisma migrateprisma generate 之类的命令
  • @prisma/client - 是 Prisma ORM 生成类型的构建器。
  • @prisma/adapter-pg - 用于将 Prisma ORM 连接到 node-postgres 的适配器
  • pg - node-postgres 连接 PostgreSQL 的 Node.js 库。
  • @types/pg - node-postgres 的 TypeScript 类型定义
  • dotenv - 加载 .env 环境变量
  1. 初始化 Prisma ORM

使用 Prisma CLI 命令创建 Prisma Schema (opens new window) 文件,从而设置 Prisma ORM 项目

$ npx prisma init --datasource-provider postgresql --output ../generated/prisma
1

这条命令会执行以下几项操作:

  • 创建一个 prisma/ 目录,其中包含一个 schema.prisma 文件,该文件包含您的数据库连接和 schema 模型。
  • 在根目录下创建一个 .env 文件,用于存放环境变量。
  • 创建用于 Prisma 配置的 prisma.config.ts 文件

配置文件 prisma.config.ts 如下,我们保持不变即可。

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
  1. 修改 .env 环境变量,连接数据库
DATABASE_URL="postgresql://username:password@localhost:5432/db-name?schema=public"
1
  1. prisma/schema.prisma 文件里定义自己的 schema
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Profile {
  id        Int      @id @default(autoincrement())
  bio       String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    Int      @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  content    String?
  published  Boolean    @default(false)
  author     User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId   Int
  categories Category[]
  createdAt  DateTime   @default(now())
  updatedAt  DateTime   @updatedAt

  @@index([authorId])
  @@index([published, createdAt])
}

model Category {
  id        Int      @id @default(autoincrement())
  name      String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  USER
  ADMIN
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  1. 创建第一个迁移以设置数据库表
$ npx prisma migrate dev --name init
1

这个命令首先会根据 DATABASE_URL 的配置创建数据库,再根据 prisma/schema.prisma 的配置创建数据库表结构,最后生成迁移记录。

├── migrations
|  ├── 20260425053014_init
|  |  └── migration.sql
|  └── migration_lock.toml
└── schema.prisma
1
2
3
4
5
  1. 生成 Prisma Client
$ npx prisma generate
1

这个命令根据 prisma/schema.prisma 文件的 output 配置选项

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}
1
2
3
4

generated/prisma 目录下生成如下文件

├── browser.ts
├── client.ts
├── commonInputTypes.ts
├── enums.ts
├── internal
|  ├── class.ts
|  ├── prismaNamespace.ts
|  └── prismaNamespaceBrowser.ts
├── models
|  ├── Category.ts
|  ├── Post.ts
|  ├── Profile.ts
|  └── User.ts
└── models.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14

需要注意的是 models 文件夹,生成了 schema 对应的对象文件。Prisma 通过这些文件将数据库操作转变为 JavaScript 对象操作

7、实例化 Prisma Client

将 Prisma ORM 驱动程序适配器的实例传递给 PrismaClient 构造函数

创建 lib/prisma.ts 文件,粘贴下面的代码

import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client";

const connectionString = `${process.env.DATABASE_URL}`;

const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });

export { prisma };
1
2
3
4
5
6
7
8
9
10

8、现在可以进行数据库操作了

import { prisma } from "./lib/prisma";

async function main() {
  const user = await prisma.user.create({
  data: {
    email: "ariadne@prisma.io",
    name: "Ariadne",
    posts: {
      create: [
        {
          title: "My first day at Prisma",
          categories: { create: { name: "Office" } },
        },
        {
          title: "How to connect to a SQLite database",
          categories: { create: [{ name: "Databases" }, { name: "Tutorials" }] },
        },
      ],
    },
  },
});

  const allUsers = await prisma.user.findMany({
    include: {
      posts: true,
    },
  });
}
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

如果想由别的库(如 Drizzle-ORM)切换到 Prisma,使用 prisma db pull 检查现有数据库

$ npx prisma db pull
1

此命令读取 DATABASE_URL 环境变量,连接到数据库,并检查数据库模式。然后,它将数据库模式从 SQL 转换为 Prisma 模式中的数据模型。

更多详情请参考 Add to Existing Project (opens new window)

# 创建表

Prisma 使用 schema.prisma 描述数据库结构。在前面我创建了 User、Profile、Post、Category 四张表。

每个 model 通常对应数据库中的一张表,模型字段对应表中的列。

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Profile {
  id        Int      @id @default(autoincrement())
  bio       String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    Int      @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  content    String?
  published  Boolean    @default(false)
  author     User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId   Int
  categories Category[]
  createdAt  DateTime   @default(now())
  updatedAt  DateTime   @updatedAt

  @@index([authorId])
  @@index([published, createdAt])
}

model Category {
  id        Int      @id @default(autoincrement())
  name      String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  USER
  ADMIN
}
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
36
37
38
39
40
41
42
43
44
45
46
47

# 列数据类型

Prisma 提供了一组与数据库无关的标量类型 (opens new window),再由数据库连接器映射为具体的列类型。

Prisma 类型 PostgreSQL 类型(默认映射) TypeScript 类型
Int integer number
BigInt bigint bigint
Float double precision number
Decimal decimal(65, 30) Prisma.Decimal
String text string
Boolean boolean boolean
DateTime timestamp(3) Date
Json jsonb Prisma.JsonValue
Bytes bytea Uint8Array

可以使用 Native database types (opens new window) 显式指定数据库类型。例如把金额限制为两位小数 @db.Decimal(10, 2)

model Product {
  id        Int      @id @default(autoincrement())
  name      String
  price     Decimal  @db.Decimal(10, 2)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
1
2
3
4
5
6
7

字段类型后面的

? 表示该字段允许为 NULL,例如 name String? 是可选字符串

[] 表示列表或关系集合。例如 posts Post[] 表示一个用户可以关联多篇文章。

# 主键、唯一约束与索引

@id@unique 用于单列约束,@@id@@unique@@index 用于模型级别的多列约束或索引:

model Post {
  id         Int      @id @default(autoincrement())
  title      String
  published  Boolean  @default(false)
  authorId   Int
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@unique([authorId, title])
  @@index([published, createdAt])
}
1
2
3
4
5
6
7
8
9
10
11
  • @id:单列主键,常配合 @default(autoincrement())
  • @unique:单列唯一约束
  • @@id:多列复合主键
  • @@unique([authorId, title]) 表示同一作者不能创建两篇同名文章。
  • @@index([published, createdAt]) 适合“按发布时间查询已发布文章”这类常用条件。索引能加快查询,但也会增加写入和存储成本,应根据实际查询模式添加。

# 默认值与自动更新时间

使用 @default() 为字段设置默认值:

model Example {
  id        Int      @id @default(autoincrement())
  code      String   @default(cuid())
  enabled   Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
1
2
3
4
5
6
7
  • autoincrement():由数据库生成递增整数。
  • cuid()uuid():生成字符串 ID。
  • now():插入记录时使用当前时间。
  • @updatedAt:每次通过 Prisma Client 更新记录时自动写入当前时间。

需要注意,@updatedAt 由 Prisma ORM 处理,并不是 PostgreSQL 的列约束;如果使用原生 SQL 更新记录,需要自己维护该字段。

# 枚举类型

枚举适合表示一组固定值。前面的 Role 会映射为 PostgreSQL 的枚举类型:

enum Role {
  USER
  ADMIN
}

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

# 关系

上面的完整 schema 包含三种常见关系。

# 一对一

UserProfile 是一对一关系:

model User {
  id        Int      @id @default(autoincrement())
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Profile {
  id        Int      @id @default(autoincrement())
  bio       String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    Int      @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Prisma 使用 @relation 定义关系

Profile.userId 是实际保存外键的关系标量字段,fields: [userId] 指定当前模型的外键,references: [id] 指定它引用 User.iduserId 必须使用 @unique,否则一个用户可以对应多个 Profile,就会变成一对多关系。

User.profile 使用 Profile?,表示用户可以暂时没有 Profile;Profile.user 没有 ?,表示每个 Profile 必须属于一个用户。删除用户时,onDelete: Cascade 会让数据库同时删除对应的 Profile。

# 一对多

UserPost 是一对多关系:

model User {
  id        Int      @id @default(autoincrement())
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

与一对一的区别是 User.posts 是数组类型 Post[]

User.posts 是不保存到数据库的 Prisma 关系字段,真正的外键保存在 Post.authorId 中。

因为通常会通过 authorId 查询文章,所以给它添加了索引。

# 多对多

PostCategory 是多对多关系。一篇文章可以属于多个分类,一个分类也可以包含多篇文章:

model Post {
  id         Int        @id @default(autoincrement())
  categories Category[]
  createdAt  DateTime   @default(now())
  updatedAt  DateTime   @updatedAt
}

model Category {
  id        Int      @id @default(autoincrement())
  name      String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这是 Prisma 的隐式多对多关系。两端都使用列表字段,Prisma Migrate 会自动创建和管理中间表,适合中间表不需要额外字段的场景。

如果需要记录分类的添加时间、排序或添加人,应改用显式中间模型:

model Post {
  id         Int                @id @default(autoincrement())
  categories CategoriesOnPosts[]
  createdAt  DateTime           @default(now())
  updatedAt  DateTime           @updatedAt
}

model Category {
  id        Int                @id @default(autoincrement())
  name      String             @unique
  posts     CategoriesOnPosts[]
  createdAt DateTime           @default(now())
  updatedAt DateTime           @updatedAt
}

model CategoriesOnPosts {
  post       Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId     Int
  category   Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
  categoryId Int
  assignedAt DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@id([postId, categoryId])
  @@index([categoryId])
}
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

@@id([postId, categoryId]) 使用两个外键组成复合主键,保证同一文章和分类只关联一次。

# 插入数据

Prisma Client 使用 create() 插入单条记录。下面在创建用户的同时创建 Profile 和文章,并为文章连接分类:

const user = await prisma.user.create({
  data: {
    email: "alice@example.com",
    name: "Alice",
    profile: {
      create: {
        bio: "TypeScript developer",
      },
    },
    posts: {
      create: {
        title: "Hello Prisma",
        content: "My first Prisma post",
        categories: {
          connectOrCreate: {
            where: {
              name: "Prisma",
            },
            create: {
              name: "Prisma",
            },
          },
        },
      },
    },
  },
  include: {
    profile: true,
    posts: {
      include: {
        categories: true,
      },
    },
  },
});
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

这是一个 Nested write (opens new window)。创建 User、Profile、Post 和 Category 的操作在同一个事务中执行,只要其中一步失败,之前的写入都会回滚。

关系数据常用以下几种写法:

  • create:创建一条新的关联记录。
  • createMany:批量创建一对多关系中的记录,但不能继续嵌套创建更深层关系。
  • connect:通过唯一字段连接已经存在的记录。
  • connectOrCreate:记录存在时连接,不存在时创建。

如果“Prisma”分类已经存在,也可以直接使用 connect

const post = await prisma.post.create({
  data: {
    title: "Prisma Relations",
    author: {
      connect: {
        email: "alice@example.com",
      },
    },
    categories: {
      connect: [
        { name: "Prisma" },
        { name: "TypeScript" },
      ],
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

connect 的条件必须能唯一定位记录,因此前面的 User.emailCategory.name 都定义了 @unique。如果目标记录不存在,整个操作会失败。

# 批量插入

PostgreSQL 可以使用 createMany() 一次插入多条记录:

const result = await prisma.category.createMany({
  data: [
    { name: "Prisma" },
    { name: "Next.js" },
    { name: "PostgreSQL" },
  ],
  skipDuplicates: true,
});

console.log(result.count);
1
2
3
4
5
6
7
8
9
10

skipDuplicates: true 会跳过违反唯一约束的记录。createMany() 返回写入数量,如果还需要返回创建后的记录,可以使用 createManyAndReturn()

const categories = await prisma.category.createManyAndReturn({
  data: [
    { name: "React" },
    { name: "Vue" },
  ],
  skipDuplicates: true,
  select: {
    id: true,
    name: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11

createManyAndReturn() 支持 PostgreSQL、CockroachDB 和 SQLite,但返回顺序不一定和输入顺序相同。顶层 createMany()createManyAndReturn() 不能使用嵌套的 createconnectconnectOrCreate 创建关系数据;复杂的关联写入应使用 create() 的 nested write,或者拆分后放入 $transaction()

# 更新数据

Prisma Client 使用 update() 更新一条记录,where 同样必须使用唯一字段:

const post = await prisma.post.update({
  where: {
    id: 1,
  },
  data: {
    title: "Prisma Relations Guide",
    published: true,
    categories: {
      connect: {
        name: "PostgreSQL",
      },
    },
  },
  include: {
    categories: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

更新文章时,updatedAt 会因为 Schema 中的 @updatedAt 自动更新。关系字段除了 connect,还支持:

  • disconnect:解除关联,但不删除关联记录。
  • set:用新的关系列表替换现有关系。
  • delete:删除关联记录。
  • updateupsert:更新关联记录,或者不存在时创建。

例如更新用户 Profile:

const user = await prisma.user.update({
  where: {
    email: "alice@example.com",
  },
  data: {
    profile: {
      upsert: {
        create: {
          bio: "Next.js developer",
        },
        update: {
          bio: "Next.js and Prisma developer",
        },
      },
    },
  },
  include: {
    profile: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 批量更新

updateMany() 根据条件更新多条记录,并返回受影响的行数:

const result = await prisma.post.updateMany({
  where: {
    authorId: 1,
    published: false,
  },
  data: {
    published: true,
  },
});

console.log(`更新了 ${result.count} 篇文章`);
1
2
3
4
5
6
7
8
9
10
11

如果希望同时返回更新后的记录,PostgreSQL 可以使用 updateManyAndReturn()

const posts = await prisma.post.updateManyAndReturn({
  where: {
    authorId: 1,
    published: false,
  },
  data: {
    published: true,
  },
  select: {
    id: true,
    title: true,
    published: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

createManyAndReturn() 一样,updateManyAndReturn() 不支持 relationLoadStrategy: "join"

# Upsert

upsert() 表示“存在则更新,不存在则创建”,适合按唯一字段同步数据:

const user = await prisma.user.upsert({
  where: {
    email: "bob@example.com",
  },
  update: {
    name: "Bobby",
  },
  create: {
    email: "bob@example.com",
    name: "Bob",
    role: "USER",
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13

# 删除数据

使用 delete() 删除一条记录:

const post = await prisma.post.delete({
  where: {
    id: 1,
  },
});
1
2
3
4
5

使用 deleteMany() 按条件批量删除:

const result = await prisma.post.deleteMany({
  where: {
    published: false,
    createdAt: {
      lt: new Date("2026-01-01"),
    },
  },
});

console.log(`删除了 ${result.count} 篇草稿`);
1
2
3
4
5
6
7
8
9
10

# 级联删除

Schema 中 Profile.userPost.author 都设置了 onDelete: Cascade

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1

因此删除 User 时,PostgreSQL 会自动删除该用户的 Profile 和 Post。隐式多对多关系中与这些 Post 关联的中间表记录也会被清理,但 Category 本身不会被删除,因为它还可能属于其他文章。

级联删除不可恢复,使用前应确认它符合业务规则。如果没有配置级联操作,删除仍被其他记录引用的 User 会触发外键约束错误。这时可以使用交互式事务,按照依赖顺序显式删除:

await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({
    where: {
      email: "alice@example.com",
    },
    select: {
      id: true,
    },
  });

  await tx.post.deleteMany({
    where: {
      authorId: user.id,
    },
  });

  await tx.profile.deleteMany({
    where: {
      userId: user.id,
    },
  });

  await tx.user.delete({
    where: {
      id: user.id,
    },
  });
});
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

事务中的任意操作失败时,所有删除都会回滚。

# 查询数据

Prisma Client 为每个模型生成了类型安全的查询方法:

  • findUnique():使用主键或唯一字段查询一条记录。
  • findUniqueOrThrow():找不到记录时抛出 PrismaClientKnownRequestError
  • findFirst():返回满足条件和排序规则的第一条记录。
  • findMany():返回满足条件的记录数组。

# 查询单条记录

通过唯一邮箱查询用户:

const user = await prisma.user.findUnique({
  where: {
    email: "alice@example.com",
  },
});
1
2
3
4
5

findUnique() 找不到记录时返回 null。如果业务上要求记录必须存在,可以使用 findUniqueOrThrow()

const user = await prisma.user.findUniqueOrThrow({
  where: {
    id: 1,
  },
});
1
2
3
4
5

findFirst() 适合查询“满足条件的第一条记录”。例如查询 Alice 最近发布的文章:

const latestPost = await prisma.post.findFirst({
  where: {
    author: {
      email: "alice@example.com",
    },
    published: true,
  },
  orderBy: {
    createdAt: "desc",
  },
});
1
2
3
4
5
6
7
8
9
10
11

# 选择返回字段

默认情况下,Prisma 返回模型的所有标量字段,但不会自动加载关系。可以使用 select 只返回需要的字段:

const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    role: true,
  },
});
1
2
3
4
5
6
7

返回类型会根据 select 自动缩小为:

const users: {
  id: number;
  email: string;
  role: Role;
}[];
1
2
3
4
5

还可以用 omit 排除敏感或不需要的字段:

const users = await prisma.user.findMany({
  omit: {
    updatedAt: true,
  },
});
1
2
3
4
5

# 过滤

findMany() 使用 where 过滤数据。Prisma 提供 equalsinltltegtgtecontainsstartsWithendsWith 等过滤条件,并使用 ANDORNOT 组合条件。

下面查询标题包含 “prisma”、已经发布,并且属于 “Prisma” 或 “PostgreSQL” 分类的文章:

const posts = await prisma.post.findMany({
  where: {
    title: {
      contains: "prisma",
      mode: "insensitive",
    },
    published: true,
    categories: {
      some: {
        name: {
          in: ["Prisma", "PostgreSQL"],
        },
      },
    },
    OR: [
      {
        content: {
          not: null,
        },
      },
      {
        author: {
          role: "ADMIN",
        },
      },
    ],
  },
});
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

someeverynone 用于过滤一对多或多对多关系:

  • some:至少一条关联记录满足条件。
  • every:所有关联记录都满足条件。
  • none:没有关联记录满足条件。

对于一对一关系,可以使用 isisNot

const users = await prisma.user.findMany({
  where: {
    profile: {
      is: {
        bio: {
          contains: "TypeScript",
        },
      },
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11

# 排序

使用 orderBy 排序。传入数组可以指定多级排序:

const posts = await prisma.post.findMany({
  where: {
    published: true,
  },
  orderBy: [
    {
      createdAt: "desc",
    },
    {
      id: "desc",
    },
  ],
});
1
2
3
4
5
6
7
8
9
10
11
12
13

第二个 id 排序能在 createdAt 相同时保持稳定顺序。

# 查询关系数据

使用 include 加载关系数据。下面查询用户、Profile、文章以及文章分类:

const users = await prisma.user.findMany({
  include: {
    profile: true,
    posts: {
      where: {
        published: true,
      },
      orderBy: {
        createdAt: "desc",
      },
      include: {
        categories: true,
      },
    },
    _count: {
      select: {
        posts: true,
      },
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

返回结果的结构大致如下:

[
  {
    "id": 1,
    "email": "alice@example.com",
    "name": "Alice",
    "role": "USER",
    "createdAt": "2026-04-27T02:00:00.000Z",
    "updatedAt": "2026-04-27T02:00:00.000Z",
    "profile": {
      "id": 1,
      "bio": "TypeScript developer",
      "userId": 1,
      "createdAt": "2026-04-27T02:00:00.000Z",
      "updatedAt": "2026-04-27T02:00:00.000Z"
    },
    "posts": [
      {
        "id": 1,
        "title": "Hello Prisma",
        "content": "My first Prisma post",
        "published": true,
        "authorId": 1,
        "createdAt": "2026-04-27T02:10:00.000Z",
        "updatedAt": "2026-04-27T02:10:00.000Z",
        "categories": [
          {
            "id": 1,
            "name": "Prisma",
            "createdAt": "2026-04-27T02:05:00.000Z",
            "updatedAt": "2026-04-27T02:05:00.000Z"
          }
        ]
      }
    ],
    "_count": {
      "posts": 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
36
37
38
39

不能在同一级参数中同时使用 selectinclude。如果既想限制用户字段,又想加载文章,应在 select 中选择关系:

const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    posts: {
      select: {
        id: true,
        title: true,
      },
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12

# 分页

Prisma 支持偏移分页和游标分页。

# 偏移分页

偏移分页使用 skiptake,适合页数不深、需要直接跳到指定页的场景:

async function getPosts(page = 1, pageSize = 10) {
  return prisma.post.findMany({
    where: {
      published: true,
    },
    orderBy: {
      createdAt: "desc",
    },
    skip: (page - 1) * pageSize,
    take: pageSize,
  });
}
1
2
3
4
5
6
7
8
9
10
11
12

它的优点是实现简单,可以直接跳转到任意页;缺点是偏移量很大时,数据库仍需要扫描并跳过前面的记录。翻页期间如果有记录插入或删除,还可能出现重复或遗漏。

# 游标分页

游标分页从上一页最后一条记录继续查询,更适合 Feed、时间线和大数据集:

async function getNextPosts(cursor?: number, pageSize = 10) {
  return prisma.post.findMany({
    where: {
      published: true,
    },
    orderBy: {
      id: "asc",
    },
    take: pageSize,
    ...(cursor
      ? {
          skip: 1,
          cursor: {
            id: cursor,
          },
        }
      : {}),
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

cursor 必须使用唯一且有序的字段。skip: 1 用于跳过游标本身,否则上一页最后一条记录会再次出现。

# 统计与分组

使用 count() 统计记录数量:

const publishedCount = await prisma.post.count({
  where: {
    published: true,
  },
});
1
2
3
4
5

使用 aggregate() 一次获得多个统计值:

const result = await prisma.post.aggregate({
  where: {
    published: true,
  },
  _count: {
    _all: true,
  },
  _max: {
    createdAt: true,
  },
  _min: {
    createdAt: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用 groupBy() 按字段分组。例如分别统计草稿和已发布文章数量:

const result = await prisma.post.groupBy({
  by: ["published"],
  _count: {
    _all: true,
  },
  orderBy: {
    published: "asc",
  },
});
1
2
3
4
5
6
7
8
9

# 事务

如果多个彼此独立的操作必须全部成功,可以使用 $transaction([])。分页接口经常需要同时查询当前页和总数:

const page = 2;
const pageSize = 10;
const where = {
  published: true,
};

const [posts, total] = await prisma.$transaction([
  prisma.post.findMany({
    where,
    orderBy: {
      createdAt: "desc",
    },
    skip: (page - 1) * pageSize,
    take: pageSize,
  }),
  prisma.post.count({
    where,
  }),
]);

const result = {
  data: posts,
  page,
  pageSize,
  total,
  totalPages: Math.ceil(total / pageSize),
};
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

需要在操作之间传递数据或编写条件逻辑时,使用前面删除章节展示的交互式事务 prisma.$transaction(async (tx) => {})。事务应尽量保持简短,避免在事务回调中执行网络请求或耗时操作。

# 数据库迁移

随着业务迭代,数据库表结构会不断变化,例如新增表、增加字段、创建索引或修改约束。数据库迁移把这些变化保存为按顺序执行的 SQL 文件,使不同开发环境、测试环境和生产环境可以复现相同的数据库结构。

Prisma Migrate 的常用命令可以分成两组:

  • 开发环境:prisma migrate dev,根据 Schema 变化创建并应用迁移。
  • 测试/生产环境:prisma migrate deploy,只应用已经提交的迁移文件。

# migrate dev

修改 schema.prisma 后,使用 migrate dev 创建迁移:

$ npx prisma migrate dev --name add_post_slug
1

migrate dev 会完成以下工作:

  1. 在 shadow database 中重新执行已有迁移,检测 schema drift。
  2. 对比当前 Prisma Schema 和数据库结构。
  3. prisma/migrations 下生成新的 SQL 迁移文件。
  4. 把尚未应用的迁移执行到开发数据库。
  5. 在数据库的 _prisma_migrations 表中记录迁移状态。

migrate dev 只应该在开发环境中运行。在 Prisma 7 中,它不会再自动执行 prisma generate 或 seed,需要时应显式运行:

$ npx prisma generate
1

# 先检查再应用迁移

自动生成的 SQL 应在应用前进行检查,特别是删除列、修改字段类型、添加非空字段等可能丢失数据的操作。使用 --create-only 只生成迁移文件,不立即应用:

$ npx prisma migrate dev --name add_post_slug --create-only
1

假设要给已有数据的 Post 添加一个必填 slug,直接添加 slug String @unique 会因为旧记录没有值而失败。更安全的方式是分阶段迁移。

1. 先添加可选字段

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  slug       String?    @unique
  content    String?
  published  Boolean    @default(false)
  author     User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId   Int
  categories Category[]
  createdAt  DateTime   @default(now())
  updatedAt  DateTime   @updatedAt

  @@index([authorId])
  @@index([published, createdAt])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

生成并检查 SQL,然后应用:

$ npx prisma migrate dev --name add_optional_post_slug --create-only
$ npx prisma migrate dev
1
2

2. 回填已有数据

可以在迁移文件中加入自定义 SQL,或者用单独的数据迁移脚本回填。PostgreSQL 的简单示例如下:

UPDATE "Post"
SET "slug" = 'post-' || "id"
WHERE "slug" IS NULL;
1
2
3

3. 改为必填字段

确认所有旧数据都有 slug 后,把字段改成 slug String @unique,再创建第二个迁移:

$ npx prisma migrate dev --name require_post_slug --create-only
1

检查生成的 SQL 是否只包含预期变化,然后运行 npx prisma migrate dev 应用迁移。不要修改已经在共享环境中应用过的迁移文件;如果需要修正,应新增迁移。

# migrate deploy

prisma/migrations 和应用代码一起提交,在 staging 或生产环境中执行:

$ npx prisma migrate deploy
1

migrate deploy 只会应用尚未执行的迁移,不会根据 Schema 生成迁移、检测 drift 或重置数据库,也不需要 shadow database。它适合放在 CI/CD 部署流程中。

生产迁移建议遵循以下流程:

  1. 在开发环境生成迁移,并人工检查 SQL。
  2. 使用接近生产数据规模的 staging 环境验证迁移和应用兼容性。
  3. 对删除列、修改类型等高风险操作准备备份和回滚方案。
  4. 部署时执行 prisma migrate deploy
  5. 部署后检查迁移状态和关键查询。

# migrate status

migrate status 会对比本地 prisma/migrations 目录和数据库中的 _prisma_migrations 表:

$ npx prisma migrate status
1

它可以发现尚未应用的迁移、数据库中存在但本地缺失的迁移,以及失败的迁移,适合部署前后检查状态。

# migrate diff

migrate diff 用于比较两个 Schema 来源。下面比较 prisma.config.ts 中配置的数据库和当前 Prisma Schema,并输出 SQL:

$ npx prisma migrate diff \
    --from-config-datasource \
    --to-schema prisma/schema.prisma \
    --script
1
2
3
4

这个命令适合排查 schema drift、预览差异或辅助处理 hotfix。它只负责比较并输出差异,不会自动应用 SQL。Prisma 7 使用 --from-config-datasource--to-config-datasource 读取配置中的 datasource。

# db push

db push 直接把 Prisma Schema 同步到数据库,不生成迁移文件:

$ npx prisma db push
1

它适合快速原型、本地实验和不需要保留变更历史的项目。因为没有可审查、可追踪的迁移历史,团队项目和生产环境通常应该使用 migrate dev + migrate deploy

如果 Prisma 检测到潜在数据丢失,db push 会发出警告。不要在不了解后果时使用 --accept-data-loss--force-reset。Prisma 7 的 db push 也不会自动执行 prisma generate

# Prisma Studio

Prisma Studio 是一个浏览器中的数据库编辑器,可以查看、过滤、新增、修改和删除 Prisma 模型中的数据:

$ npx prisma studio
1

它会读取 prisma.config.tsschema.prisma 和 datasource 配置,然后启动本地 Web 服务并打开浏览器。

如果配置文件不在默认位置,可以通过 --config 指定:

$ npx prisma studio --config ./prisma.config.ts
1

Prisma Studio 很适合在本地开发时检查关联数据、验证迁移结果和手动准备测试数据。它可以直接修改数据库,因此连接生产数据库时必须格外谨慎,也不应该把未受身份验证和网络访问控制保护的 Studio 暴露到公网。

# References