Skip to content

Prisma 入门指南:像写代码一样操作数据库

如果你受够了写 SQL 语句的繁琐,厌倦了传统 ORM 的类型丢失,Prisma 可能正是你在寻找的工具。这篇教程将带你从零掌握 Prisma,用更优雅的方式与数据库交互。

本文档适用于 Prisma 7+ 版本


前言

传统的数据库操作方式总是让人割裂:写好了 TypeScript 类型定义,数据库表结构又是另一套描述;改一个字段,要同时维护三处代码;最离谱的是,写错了字段名要等到运行时才能发现。

Prisma 试图解决这些问题。它用一份声明式的 schema.prisma 文件定义数据模型,然后自动生成 TypeScript 类型的数据库客户端。这意味着:当你改 Schema 时,类型会自动更新;当你写查询时,IDE 会告诉你字段是否正确。

这篇教程按学习顺序展开,从环境搭建到实战 CRUD,每个章节都有可运行的代码示例。建议你跟着敲一遍,效果更好。


目录

  1. 环境搭建:5 分钟跑起来
  2. 模型定义:Prisma 的核心
  3. 数据迁移:安全地变更数据库结构
  4. 客户端:代码里怎么用
  5. CRUD 实战:增删改查
  6. 关系操作:处理表关联
  7. 跨表查询:多表联合检索
  8. 事务调用:原子操作与一致性
  9. 种子数据:快速填充测试数据
  10. Prisma 优势与安全性
  11. 进阶技巧:错误处理与性能

1. 环境搭建:5 分钟跑起来

安装

首先创建一个新项目,全程只需要几个命令:

bash
mkdir my-prisma-app && cd my-prisma-app
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npx prisma init
  • prisma(开发依赖):命令行工具,只负责生成客户端、执行迁移等开发阶段的工作
  • @prisma/client(生产依赖):运行时的数据库客户端,应用通过它与数据库交互

Tip:官方 npx prisma init 命令会自动创建 prisma/schema.prisma.env 文件,并提示你配置数据库连接。

执行 npx prisma init 后,项目中会新增以下文件:

my-prisma-app/
├── prisma/
│   ├── schema.prisma    ← 数据模型定义文件
│   └── config.ts        ← 数据库连接配置(Prisma 7+)
├── .env                 ← 环境变量
└── package.json

配置数据库连接

数据库连接配置在 .env 文件和 prisma.config.ts 中管理。

SQLite 的连接 URL 格式为 file:./dev.db(相对路径)或 file:/absolute/path/to/dev.db(绝对路径):

env
DATABASE_URL="file:./sqlite.db"

Tip:SQLite 数据库文件会在首次迁移时自动创建。./sqlite.db 表示相对于项目根目录。

初始化 Schema

schema.prisma 定义数据模型,prisma.config.ts 管理连接配置:

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

datasource db {
  provider = "sqlite"
  relationMode = "prisma"
}
typescript
// 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"),
  },
})

注意:确保已安装 dotenv 包(npm install dotenv),用于加载 .env 文件中的环境变量。


2. 模型定义:Prisma 的核心

什么是 Schema

schema.prisma 是 Prisma 的核心。你在这里定义所有的数据模型(相当于 SQL 的 CREATE TABLE),Prisma 会自动帮你生成对应的 TypeScript 类型和数据库表结构。

先来看一个完整的例子,然后逐一拆解:

prisma
// prisma/schema.prisma

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

datasource db {
  provider = "sqlite"
  relationMode = "prisma"
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String?
  age       Int       @default(0)
  posts     Post[]
  deletedAt DateTime? // 软删除标记
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  authorId  Int
  user      User?   @relation(fields: [userId], references: [id])
  userId    Int?
}

字段类型

Prisma 定义了一套自己的类型系统,映射到各数据库的原生类型:

Prisma 类型说明典型用途
String字符串名字、邮箱、文本内容
Int整数ID、年龄、数量
BigInt长整数大数值
Float浮点数价格、经纬度
Boolean布尔值是否发布、是否激活
DateTime日期时间创建时间、更新时间
JsonJSON 对象灵活的结构化数据
Bytes二进制文件、图片
Decimal精确小数货币、精确计算

字段修饰符

修饰符控制字段的行为,就像给字段加了一层能力:

修饰符说明示例
?可选字段,允许 nullname String?
@default(value)默认值age Int @default(0)
@unique唯一约束email String @unique
@id主键id Int @id
@default(autoincrement())自增主键id Int @id @default(autoincrement())
@updatedAt自动更新时间戳updatedAt DateTime @updatedAt
@default(now())默认当前时间createdAt DateTime @default(now())

注意@updatedAt 会在每次记录更新时自动刷新,而 @default(now()) 只在创建时设置一次。

关系定义

数据库表之间的关系用 @relation 声明。常见的有三种:

一对多(一个用户有多篇文章):

prisma
model User {
  id    Int    @id @default(autoincrement())
  posts Post[]  // 一对多:数组表示多方
}

model Post {
  id       Int  @id @default(autoincrement())
  user     User @relation(fields: [userId], references: [id])
  userId   Int  // 外键
  authorId Int  // 注意:实际代码中 authorId 是 Int 不是可选的
}

一对一(一个用户有一份资料):

prisma
model User {
  id      Int      @id @default(autoincrement())
  profile Profile?  // 一对一,可选
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String
  user   User   @relation(fields: [userId], references: [id])
  userId Int    @unique  // 一对一需要外键唯一
}

多对多(一篇文章有多个标签):

prisma
model Post {
  id    Int    @id @default(autoincrement())
  tags  Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

Prisma 会自动创建隐式的连接表,你不需要手动管理。

关系模式:物理外键 vs 逻辑外键

Prisma 的 @relation 支持两种关系管理模式:

1. 物理外键(foreignKeys)

  • 在数据库层面创建真实的外键约束
  • 依赖底层数据库的外键功能(PostgreSQL、MySQL)
  • 数据库自动维护引用完整性

2. 逻辑外键(prisma)

  • 不创建数据库外键
  • 仅在 Prisma Client 层面维护关系
  • 由应用层确保数据一致性

注意:SQLite 不支持外键约束,只能使用逻辑外键模式(relationMode = "prisma")。

配置示例

prisma
// PostgreSQL/MySQL 物理外键模式(使用数据库外键)
datasource db {
  provider     = "postgresql"
  relationMode = "foreignKeys"
}

// SQLite 或需要逻辑外键时
datasource db {
  provider     = "sqlite"
  relationMode = "prisma"
}

两种模式对比:

特性foreignKeysprisma
数据库外键✅ 创建❌ 不创建
数据一致性数据库级约束应用级约束
迁移支持不需要迁移
写入性能可能有约束开销更快
跨表查询SQL JOINPrisma Client

Tip:关系模式与 provider 无关,PostgreSQL/MySQL 也可以使用 prisma 模式。SQLite 由于不支持外键约束,只能使用 prisma 模式。

枚举类型

prisma
model User {
  id    Int    @id @default(autoincrement())
  name  String
  role  Role   @default(USER)
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

Tip:SQLite 原生不支持枚举,Prisma 会将其存储为 Text 并在客户端做类型校验。

软删除支持

实际项目中,推荐使用软删除保留数据:

prisma
model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String?
  age       Int       @default(0)
  posts     Post[]
  deletedAt DateTime? // 软删除标记,为 null 表示未删除
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

3. 数据迁移:安全地变更数据库结构

迁移是什么

迁移(Migration)是将 Schema 变更同步到数据库的 SQL 脚本。每次你修改 schema.prisma,迁移系统会生成对应的 SQL 语句,确保数据库结构与 Schema 保持一致。

两个核心命令

命令场景特点
migrate dev开发环境创建迁移文件 + 执行到数据库
db push快速原型直接同步,不生成迁移文件
migrate deploy生产环境只执行已有迁移文件

SQLite 特别说明:SQLite 使用逻辑外键模式(relationMode = "prisma"),因此迁移时不会在数据库层面创建外键约束。

开发环境推荐用 migrate dev

bash
npx prisma migrate dev --name init

--name 参数是本次迁移的描述,比如 --name add_user_table--name add_avatar_field。这条命令会:

  1. prisma/migrations/ 下创建迁移文件夹
  2. 执行 SQL 到数据库
  3. 自动运行 prisma generate

快速原型用 db push

bash
npx prisma db push

适合不想管理迁移文件的时候,但不推荐在生产环境使用

生产环境用 migrate deploy

bash
npx prisma migrate deploy

它只执行已有的迁移文件,不会生成新的。确保迁移文件已经提交到 Git。

分阶段开发流程

开发流程建议分为三个阶段,避免迁移文件碎片化:

阶段一:开发调试(频繁调整 Schema)

bash
npx prisma db push
  • 直接同步数据库结构,不生成迁移文件
  • 适合频繁修改字段、测试功能阶段
  • 开发完成后删除本地数据库重新来过也很方便

阶段二:提测前合并(准备发布)

bash
npx prisma migrate dev --create-only --name init
  • 生成一个完整的迁移文件
  • 将之前零散的调整合并为一次迁移
  • 提交到 Git 后再提测

阶段三:正式开发(功能稳定后)

bash
npx prisma migrate dev --name add_user_avatar
  • 每次 Schema 变更生成一个迁移文件
  • 自动执行迁移 + 生成类型
  • 适合功能开发完成、进入维护阶段

迁移文件管理原则

原则说明
开发调试用 db push不留痕迹,随时重来
提测/发布前合并--create-only 生成单一迁移
上线后谨慎变更每次变更加密记录,保留历史

查看迁移状态

bash
npx prisma migrate status

输出会显示哪些迁移已应用、哪些待执行。

上线部署流程

生产环境部署遵循以下步骤:

第一步:准备迁移文件

确保所有迁移文件已提交到 Git,并在生产服务器拉取最新代码:

bash
git pull origin main

第二步:执行数据库迁移

bash
npx prisma migrate deploy
  • 只执行已有迁移,不会生成新文件
  • 如果迁移已在生产数据库执行过,会自动跳过
  • 建议在应用启动前或发布的早期阶段执行

第三步:生成客户端类型

bash
npx prisma generate
  • 部署完成后在生产服务器执行
  • 或在 CI/CD 流程中打包到镜像里

第四步:启动应用

部署脚本完成迁移后,正常启动应用即可。

回滚方案

场景处理方式
迁移执行失败migrate deploy 会中止,不会执行部分迁移
需要回滚代码使用 Git 回滚,迁移文件保留在 migrations/
需要回滚数据库手动执行逆 SQL(如有),或使用备份恢复
迁移文件损坏从 Git 历史恢复正确的迁移文件,重新 migrate deploy

命令场景对照表

场景推荐命令迁移文件说明
开发调试prisma db push❌ 不生成快速同步,随时重来
功能开发prisma migrate dev✅ 生成并执行完整迁移流程
只生成迁移prisma migrate dev --create-only✅ 只生成不执行,用于合并迁移
生产部署prisma migrate deploy— 执行已有不生成,只执行
生成类型prisma generate只生成客户端类型

4. 客户端:代码里怎么用

回答安装时埋下的伏笔

第一章我们注意到一个细节:

bash
npm install prisma --save-dev

prisma 装在了 devDependencies。但运行时代码里我们需要在某处导入 PrismaClient——这个客户端从哪里来?

答案:通过 prisma generate 命令,根据 schema 中的 output 配置生成客户端代码到你指定的位置(默认是 ./generated)。

两个概念的关系

概念作用
prisma (CLI)命令行工具:init、generate、migrate 等命令
生成的客户端根据 schema 自动生成的 TypeScript 类型和 CRUD 方法
npm install prisma --save-dev     ← 安装 CLI 工具
npx prisma generate              ← 生成客户端代码到 output 目录

生成客户端

Schema 定义好后,运行 generate 命令:

bash
npx prisma generate

这条命令做了三件事:

  1. 读取 schema.prisma 的模型定义
  2. 根据模型生成对应的 TypeScript 类型和 CRUD 方法
  3. 输出到 schema 中 output 指定的目录(默认为 ./generated

生成的内容包含:

  • 每个模型的 TypeScript 类型定义
  • PrismaClient 类及其所有方法
  • 自动推导的智能提示类型

每次修改 Schema 后都要重新 generate,否则客户端代码与数据库结构会不一致。

初始化 PrismaClient

生成客户端后,就可以在代码里使用了。推荐在 src/prisma/index.ts 中创建统一出口:

typescript
import { PrismaClient } from '../../prisma/generated'

// 直接实例化使用
const prisma = new PrismaClient()

export default prisma

注意:实际项目中,生成的客户端路径取决于 schema 中 output 的配置。本项目配置为 ./generated,所以导入路径是 ../../prisma/generated

单例模式(开发环境推荐)

开发模式下修改代码会触发热更新,Node.js 会重新执行模块。如果每次都 new PrismaClient(),会创建多个数据库连接,最终耗尽连接池。用单例模式可以避免这个问题:

typescript
import { PrismaClient } from '../../prisma/generated'

declare global {
  var prisma: PrismaClient | undefined
}

// 单例模式:避免开发模式热更新导致连接耗尽
const prisma = globalThis.prisma || new PrismaClient()

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

export default prisma

其他文件使用

typescript
import prisma from './prisma'

// 之后所有数据库操作都通过 prisma 对象
const users = await prisma.user.findMany()
console.log(users)

prisma 对象上挂载了所有模型的 CRUD 方法:

  • prisma.user.findMany() — 查询用户列表
  • prisma.post.create({ data: {...} }) — 创建文章
  • 以此类推……

Tip:敲 prisma. 时 IDE 会弹出所有可用方法,这就是类型自动推导的好处。


5. CRUD 实战:增删改查

Create(创建)

typescript
import prisma from './prisma'

// 创建一条记录
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    age: 25,
  },
})

// 批量创建
const result = await prisma.user.createMany({
  data: [
    { email: 'bob@example.com', name: 'Bob', age: 30 },
    { email: 'carol@example.com', name: 'Carol', age: 28 },
  ],
})
console.log(`创建了 ${result.count} 条记录`)

// 嵌套创建(创建用户的同时创建文章)
// 注意:Post 模型中 authorId 是 Int 类型(必填),userId 是 Int?(可选)
const postWithAuthor = await prisma.post.create({
  data: {
    title: 'Prisma 入门',
    content: '学习 Prisma 的第一篇笔记',
    authorId: 1,  // 使用 authorId 关联用户
  },
  include: {
    user: true,  // 同时返回关联的用户信息
  },
})

Read(查询)

typescript
// 查询单条(按主键)
const user1 = await prisma.user.findUnique({
  where: { id: 1 },
})

// 按唯一字段查询
const userByEmail = await prisma.user.findUnique({
  where: { email: 'alice@example.com' },
})

// 查询多条
const allUsers = await prisma.user.findMany()

// 条件查询
const activeUsers = await prisma.user.findMany({
  where: {
    age: { gte: 18 },
    deletedAt: null,  // 只查询未删除的
  },
})

// 排序 + 分页
const paginatedUsers = await prisma.user.findMany({
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0,
})

// 完整分页查询(含总数)
const findUsersWithPagination = async (page: number = 1, pageSize: number = 10) => {
  const [users, total] = await prisma.$transaction([
    prisma.user.findMany({
      skip: (page - 1) * pageSize,
      take: pageSize,
      orderBy: { id: 'asc' },
    }),
    prisma.user.count(),
  ])
  return {
    list: users,
    total,
    page,
    pageSize,
    totalPages: Math.ceil(total / pageSize),
  }
}

// 使用示例
const result = await findUsersWithPagination(1, 5)
// 返回: { list: [...], total: 20, page: 1, pageSize: 5, totalPages: 4 }

// 只返回指定字段
const names = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
  },
})

// 模糊搜索(SQLite/PostgreSQL 支持)
const searchUsers = await prisma.user.findMany({
  where: {
    name: { contains: 'ali' },
  },
})

常用查询条件:

typescript
const users = await prisma.user.findMany({
  where: {
    // 比较运算
    age: { equals: 25 },
    age: { not: 25 },
    age: { gt: 18 },
    age: { gte: 18 },
    age: { lt: 65 },
    age: { lte: 65 },
    age: { in: [18, 25, 30] },
    age: { notIn: [18, 25] },

    // 字符串匹配
    name: { contains: 'ali' },
    name: { startsWith: 'A' },
    name: { endsWith: 'e' },

    // 逻辑组合
    AND: [{ age: { gte: 18 } }, { deletedAt: null }],
    OR: [{ name: 'Alice' }, { name: 'Bob' }],
    NOT: { deletedAt: null },
  },
})

Update(更新)

typescript
// 更新单条
const updatedUser = await prisma.user.update({
  where: { id: 1 },
  data: {
    name: 'Alice Updated',
    age: { increment: 1 },  // 原子操作:age = age + 1
  },
})

// 批量更新
await prisma.user.updateMany({
  where: { age: { lt: 18 }, deletedAt: null },
  data: { age: 18 },
})

// upsert(存在则更新,不存在则创建)
const upserted = await prisma.user.upsert({
  where: { email: 'alice@example.com' },
  update: { name: 'Updated Name' },
  create: { email: 'alice@example.com', name: 'New Name' },
})

Tipincrementdecrementmultiplydivide 是原子操作,在并发场景下比先读后写更安全。

Delete(删除)

typescript
// 删除单条(硬删除)
await prisma.user.delete({
  where: { id: 1 },
})

// 批量删除(硬删除)
await prisma.user.deleteMany({
  where: { age: { lt: 18 } },
})

// 删除所有(慎用!)
await prisma.user.deleteMany({})

6. 关系操作:处理表关联

嵌套查询(include)

查询用户时同时获取关联的文章:

typescript
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 5,
    },
  },
})

// 嵌套 include(用户 -> 文章 -> 作者)
const data = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        user: true,
      },
    },
  },
})

嵌套创建

创建用户时同时创建文章:

typescript
const user = await prisma.user.create({
  data: {
    email: 'author@example.com',
    name: 'Author',
    posts: {
      create: [
        { title: '第一篇', content: '内容...', published: false, authorId: 1 },
        { title: '第二篇', content: '内容...', published: true, authorId: 1 },
      ],
    },
  },
  include: { posts: true },
})

嵌套更新(createMany)

typescript
// 使用 createMany 批量创建文章
const user = await prisma.user.update({
  where: { id: 1 },
  data: {
    posts: {
      createMany: {
        data: [
          { title: '标题', authorId: 1 },
        ],
      },
    },
  },
  include: { posts: true },
})

统计关联数量

typescript
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    _count: {
      select: { posts: { where: { published: true } } },
    },
  },
})
console.log(user._count.posts)  // 已发布文章数量

7. 跨表查询:多表联合检索

使用场景

跨表查询(又称联表查询)用于同时从多个相关表中获取数据。在传统 SQL 中需要用 JOIN 实现,Prisma 提供了更直观的方式让你无需手写 JOIN 语法。

Fluent API 链式查询

Fluent API 允许你用链式调用的方式逐步深入关联表:

typescript
// 从 User 出发,查询该用户的第一篇文章的作者信息
const result = await prisma.user.findUnique({
  where: { id: 1 },
}).posts.findFirst().user()

// 逐步拆解:
// 1. prisma.user.findUnique({ where: { id: 1 } }) - 找到用户
// 2. .posts.findFirst() - 获取该用户的第一篇文章
// 3. .user() - 获取这篇文章关联的用户

隐式联表查询(推荐)

Prisma 支持在 where 条件中直接引用关联表的字段:

typescript
// 查询所有包含已发布文章的用户
const users = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        published: true,
      },
    },
  },
})

// 查询所有文章都被发布过的用户(所有文章都是 published=true)
const users = await prisma.user.findMany({
  where: {
    posts: {
      every: {
        published: true,
      },
    },
  },
})

// 查询没有任何文章的用户
const users = await prisma.user.findMany({
  where: {
    posts: {
      none: {},
    },
  },
})

四种过滤条件对比:

条件说明示例
some至少有一个满足posts.some: { published: true } — 至少有一篇已发布
every所有都满足posts.every: { published: true } — 所有文章都已发布
none没有满足的posts.none: { published: true } — 没有已发布的文章
is关联对象满足posts.is: { id: 1 } — 关联对象的 ID 为 1
isNot关联对象不满足posts.isNot: { id: 1 } — 关联对象的 ID 不为 1

深度嵌套查询

可以多层嵌套,查询更深层的关系:

typescript
// 查询:获取用户 -> 该用户的文章 -> 文章的作者 -> 作者的文章
const result = await prisma.user.findMany({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        user: {
          include: {
            posts: {
              select: { id: true, title: true },
            },
          },
        },
      },
    },
  },
})

聚合与分组

typescript
// 统计每个用户的文章数量
const userWithPostCount = await prisma.user.findMany({
  include: {
    _count: {
      select: { posts: true },
    },
  },
})

// 按文章数量排序
const sortedUsers = await prisma.user.findMany({
  include: {
    _count: {
      select: { posts: true },
    },
  },
  orderBy: {
    posts: {
      _count: 'desc',
    },
  },
})

跨表更新

通过 updateconnectdisconnect 操作关联关系:

typescript
// 将已有的文章关联到用户
await prisma.user.update({
  where: { id: 1 },
  data: {
    posts: {
      connect: [{ id: 10 }, { id: 11 }],
    },
  },
})

// 取消关联
await prisma.user.update({
  where: { id: 1 },
  data: {
    posts: {
      disconnect: [{ id: 10 }],
    },
  },
})

关联不存在时创建

使用 connectOrCreate 在关联存在时连接,不存在时创建:

typescript
// 查询或创建分类,然后关联到文章
const post = await prisma.post.update({
  where: { id: 1 },
  data: {
    categories: {
      connectOrCreate: {
        where: { name: '技术' },
        create: { name: '技术', slug: 'tech' },
      },
    },
  },
})

8. 事务调用:原子操作与一致性

为什么需要事务

事务确保一组数据库操作要么全部成功,要么全部失败。当业务逻辑涉及多个表的多步操作时,事务是保证数据一致性的关键。

典型场景:

  • 转账:从账户 A 扣款,向账户 B 存款,必须同时成功或同时失败
  • 创建订单:创建订单记录、扣减库存、记录日志,任何一步失败都应该回滚
  • 注册用户:创建用户记录、初始化用户配置、发送欢迎邮件

Prisma 事务的三种方式

方式一:交互式事务(推荐)

$transaction 接收一个回调函数,在事务上下文中执行所有操作:

typescript
async function createUserWithPosts(email: string, titles: string[]) {
  return await prisma.$transaction(async (tx) => {
    // 所有操作都在同一个事务中
    const user = await tx.user.create({
      data: { email, name: email.split('@')[0] },
    })

    const posts = await tx.post.createMany({
      data: titles.map((title) => ({
        title,
        authorId: user.id,
        published: false,
      })),
    })

    return { user, postCount: posts.count }
  })
}

// 调用
const result = await createUserWithPosts('alice@example.com', ['标题1', '标题2'])

特点

  • 如果回调中任何操作失败,整个事务自动回滚
  • 可以在回调中使用 tx 对象执行任何 Prisma 操作
  • 支持异步操作

方式二:自动提交事务

将操作数组传给 $transaction,自动打包成事务:

typescript
const [user, post, log] = await prisma.$transaction([
  prisma.user.create({
    data: { email: 'new@example.com', name: 'New User' },
  }),
  prisma.post.create({
    data: { title: '新文章', authorId: 1, published: false },
  }),
  prisma.activityLog.create({
    data: { action: 'user_created', userId: 1 },
  }),
])

特点

  • 操作并行执行
  • 所有操作必须成功,否则自动回滚
  • 不能在操作之间传递数据(如用第一个操作的结果作为第二个操作的输入)

方式三:序列化事务(isolationLevel)

typescript
await prisma.$transaction(async (tx) => {
  // ... 操作
}, {
  isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
})

可选的隔离级别:

  • ReadCommitted(默认)
  • Serializable(最强隔离,防止所有并发问题)

注意:不是所有数据库都支持所有隔离级别。SQLite 只支持 Serializable

嵌套事务(Savepoint)

在交互式事务中可以创建保存点,实现部分回滚:

typescript
await prisma.$transaction(async (tx) => {
  // 操作 1
  await tx.user.create({ data: { email: 'a@test.com', name: 'A' } })

  try {
    await tx.$transaction(async (savepoint) => {
      // 保存点内的操作
      await savepoint.post.create({ data: { title: 'Test', authorId: 999 } })
      // 这个会失败,因为 authorId 999 不存在
    })
  } catch (e) {
    // 保存点回滚,但外部事务继续
    console.log('保存点回滚,继续执行')
  }

  // 操作 2(继续执行)
  await tx.user.create({ data: { email: 'b@test.com', name: 'B' } })
})

实际业务场景示例

场景一:转账

typescript
async function transfer(fromId: number, toId: number, amount: number) {
  return await prisma.$transaction(async (tx) => {
    // 1. 检查发送方余额
    const fromAccount = await tx.account.findUnique({ where: { id: fromId } })
    if (!fromAccount || fromAccount.balance < amount) {
      throw new Error('余额不足')
    }

    // 2. 扣减发送方余额
    await tx.account.update({
      where: { id: fromId },
      data: { balance: { decrement: amount } },
    })

    // 3. 增加接收方余额
    await tx.account.update({
      where: { id: toId },
      data: { balance: { increment: amount } },
    })

    // 4. 记录转账日志
    await tx.transferLog.create({
      data: {
        fromId,
        toId,
        amount,
        status: 'completed',
      },
    })

    return { success: true }
  })
}

场景二:创建订单

typescript
async function createOrder(userId: number, items: { productId: number; quantity: number }[]) {
  return await prisma.$transaction(async (tx) => {
    // 1. 计算订单总金额并验证库存
    let totalAmount = 0
    for (const item of items) {
      const product = await tx.product.findUnique({
        where: { id: item.productId },
      })
      if (!product) throw new Error(`商品 ${item.productId} 不存在`)
      if (product.stock < item.quantity) {
        throw new Error(`商品 ${product.name} 库存不足`)
      }
      totalAmount += product.price * item.quantity
    }

    // 2. 创建订单
    const order = await tx.order.create({
      data: {
        userId,
        totalAmount,
        status: 'pending',
      },
    })

    // 3. 创建订单项并扣减库存
    for (const item of items) {
      await tx.orderItem.create({
        data: {
          orderId: order.id,
          productId: item.productId,
          quantity: item.quantity,
        },
      })
      await tx.product.update({
        where: { id: item.productId },
        data: { stock: { decrement: item.quantity } },
      })
    }

    return order
  })
}

事务性能考虑

场景建议
短事务事务内操作越少越好,减少锁竞争
长事务考虑拆分为多个小事务,用补偿机制保证一致性
并发写入使用 Serializable 隔离级别防止幻读
批量操作createMany 代替循环单条插入

常见错误处理

typescript
try {
  await prisma.$transaction(async (tx) => {
    // ... 操作
  })
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    switch (error.code) {
      case 'P2034':  // 事务被回滚
        console.error('事务冲突,请重试')
        break
      default:
        console.error('数据库错误:', error.message)
    }
  }
}

常见事务错误码:P2034(事务被回滚或超期)、P2028(事务 API 错误)。


9. 种子数据:快速填充测试数据

配置 seed 命令

package.json 中添加:

json
"prisma": {
  "seed": "tsx prisma/seed.ts"
}

需要安装 tsx(已安装):

bash
npm install -D tsx

编写 seed 脚本

创建 prisma/seed.ts

typescript
import { PrismaClient } from '../src/prisma/generated'

const prisma = new PrismaClient()

async function main() {
  console.log('开始填充种子数据...')

  // 按依赖顺序清理(先删子表再删主表)
  await prisma.$transaction([
    prisma.post.deleteMany(),
    prisma.user.deleteMany(),
  ])

  // 创建用户
  const alice = await prisma.user.create({
    data: {
      email: 'alice@example.com',
      name: 'Alice',
      age: 25,
    },
  })

  // 批量创建文章
  await prisma.post.createMany({
    data: [
      {
        title: 'Prisma 入门指南',
        content: '学习 Prisma ORM 的第一课',
        published: true,
        authorId: alice.id,
      },
      {
        title: 'TypeScript 最佳实践',
        content: 'TypeScript 开发经验总结',
        published: true,
        authorId: alice.id,
      },
      {
        title: '草稿文章',
        content: '未完成的草稿',
        published: false,
        authorId: alice.id,
      },
    ],
  })

  console.log('种子数据填充完成!')
}

main()
  .catch((e) => {
    console.error('填充失败:', e)
    process.exit(1)
  })
  .then(() => prisma.$disconnect())
  .catch(() => process.exit(1))

运行 seed

bash
npx prisma db seed

Tipmigrate reset 会重置数据库并重新运行 seed,适合开发阶段反复测试。


10. Prisma 优势与安全性

Prisma 的核心优势总结

1. 端到端类型安全

传统的数据库操作方式存在一个普遍问题:数据库表结构、TypeScript 类型、后端 API 响应这三者之间没有自动同步机制。Prisma 的解决方案是:用 Schema 定义一切,然后自动生成类型

当你修改 Schema 后,生成的类型会自动更新。所有使用这些类型的地方都会立即获得类型检查,IDE 会在你写代码时提示错误,而不是等到运行时才发现问题。

2. 简洁优雅的 API

Prisma Client 的 API 设计非常直观,用链式调用的方式构建查询。对比手写 SQL,Prisma 的查询构建器让代码更具可读性,同时也避免了字符串拼接带来的语法错误风险。

3. 强大的迁移系统

Prisma Migrate 提供了安全、可追溯的数据库版本管理。迁移文件记录了每次 Schema 变更的 SQL 脚本,可以提交到 Git 进行版本管理。

4. 开发体验优化

  • npx prisma studio 提供可视化数据库管理界面
  • IDE 自动补全所有可用方法
  • 类型推导让代码更可靠
  • Schema 变更后自动重新生成客户端

安全性保障

防止 SQL 注入

Prisma Client 所有查询都使用参数化查询,不需要手动转义用户输入:

typescript
// 安全的写法 - Prisma 自动处理参数化
const user = await prisma.user.findUnique({
  where: { email: userInput },
})

// Raw Query 同样安全
const users = await prisma.$queryRaw`
  SELECT * FROM User WHERE age > ${minAge}
`

敏感数据保护

  • 环境变量存储数据库连接字符串,不硬编码在代码中
  • .env 文件不提交到 Git,密码和 API Key 得到保护
  • Prisma 7+ 配置统一在 prisma.config.ts 中管理

其他安全措施

安全特性说明
连接池管理自动管理数据库连接,防止连接耗尽
查询超时可以配置查询最大执行时间
限流保护配合 API 限流防止滥用

11. 进阶技巧:错误处理与性能

错误处理

typescript
import { Prisma } from '../src/prisma/generated'

try {
  const user = await prisma.user.create({
    data: { email: 'existing@example.com' },
  })
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    switch (e.code) {
      case 'P2002':
        console.error('唯一约束冲突:该邮箱已被注册')
        break
      case 'P2025':
        console.error('记录不存在')
        break
      case 'P2003':
        console.error('外键约束失败')
        break
    }
  }
}

常见错误码对照表:

错误码说明常见场景
P2002唯一约束冲突插入重复的 unique 字段值
P2025记录不存在更新/删除时找不到记录
P2003外键约束失败删除有关联数据的记录
P2034事务冲突并发更新同一记录
P2028事务 API 错误事务超时或回滚

性能优化

技巧说明
批量操作createMany 代替循环 create
字段选择select 只查需要的字段
分页takeskip 避免大结果集
索引对高频查询字段加 @index
prisma
model User {
  id     Int     @id @default(autoincrement())
  email  String  @unique
  name   String
  age    Int

  @@index([age])       // 单字段索引
  @@index([name, age]) // 复合索引
}

原始 SQL

当 Prisma 的查询构建器不够用时,可以用 $queryRaw

typescript
// 查询(返回类型需手动指定)
const users = await prisma.$queryRaw<User[]>`
  SELECT * FROM User WHERE age > ${18}
`

// 执行(返回受影响行数)
const result = await prisma.$executeRaw`
  UPDATE User SET age = age + 1 WHERE id = ${1}
`

注意:使用模板字符串插值 ${} 是安全的,Prisma 会自动处理参数化查询,防止 SQL 注入。

连接池配置

typescript
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: env("DATABASE_URL"),
    },
  },
  log: ['query', 'info', 'warn', 'error'],
})

总结

这篇教程覆盖了 Prisma 的核心知识点:

  1. Schema 是核心:一份文件定义模型,生成类型安全的客户端
  2. 模型定义:掌握字段类型、修饰符、关系声明
  3. 迁移:开发用 migrate dev,生产用 migrate deploy
  4. 客户端:正确导入生成的客户端,避免连接耗尽
  5. CRUD:Create / Read / Update / Delete 的标准写法
  6. 关系操作include 嵌套查询、createMany 批量创建
  7. 跨表查询:Fluent API、隐式联表、聚合分组
  8. 事务调用:原子操作保证数据一致性
  9. 种子数据:测试数据快速填充
  10. Prisma 优势:类型安全、优雅 API、迁移系统、开发体验
  11. 进阶技巧:错误处理、性能优化、原始 SQL

下一步

有问题或发现教程中的疏漏,欢迎交流讨论。


附录

常用命令速查

命令说明
npx prisma init初始化项目
npx prisma generate生成客户端代码
npx prisma migrate dev开发环境迁移(创建并执行)
npx prisma migrate deploy生产环境部署(只执行已有)
npx prisma migrate reset重置数据库(删除并重建)
npx prisma db push直接同步 Schema(不生成迁移)
npx prisma db seed运行种子数据
npx prisma db execute执行原生 SQL
npx prisma studio可视化数据库管理
npx prisma validate验证 Schema 语法
npx prisma format格式化 Schema
npx prisma version查看 Prisma 版本信息

Schema 类型对照

Prisma 类型SQLitePostgreSQLMySQL
StringTEXTVARCHARVARCHAR
IntINTEGERINTEGERINT
FloatREALDOUBLEDOUBLE
BooleanINTEGERBOOLEANBOOLEAN
DateTimeTEXTTIMESTAMPDATETIME
JsonTEXTJSONBJSON

注意:SQLite 使用 TEXT 存储 String、DateTime、Json 类型,因为它没有对应的原生类型。Prisma 会在客户端自动进行类型转换。

实际项目结构参考

本项目的实际文件结构:

npm-package/prisma/
├── prisma/
│   ├── schema.prisma        # 数据模型定义
│   ├── config.ts            # Prisma 配置
│   ├── generated/           # 生成的客户端(由 prisma generate 生成)
│   │   ├── client.ts
│   │   ├── models/
│   │   └── ...
│   └── migrations/          # 迁移文件
├── src/
│   ├── index.ts             # 服务层入口
│   ├── query.ts             # CRUD 示例
│   └── services/
│       ├── index.ts         # 服务导出
│       ├── base.service.ts   # 基础服务类(含软删除)
│       └── user.service.ts   # 用户服务
├── .env                     # 环境变量
└── package.json

服务层设计参考

实际项目中推荐的服务层封装模式:

typescript
// src/services/base.service.ts - 基础服务类
export class BaseService<T extends ISoftDelete, TCreate, TUpdate> {
  // 包含软删除过滤的 CRUD 方法
  async findMany(args?: {...}): Promise<T[]> {
    // 自动过滤 deletedAt: null
  }

  async softDelete(where: {...}): Promise<T> {
    // 软删除:设置 deletedAt
  }

  async restore(where: {...}): Promise<T> {
    // 恢复软删除
  }
}

// src/services/user.service.ts - 用户服务
export class UserService extends BaseService<User, CreateUserInput, UpdateUserInput> {
  async findByEmail(email: string): Promise<User | null>
  async emailExists(email: string, excludeId?: number): Promise<boolean>
  async getUserList(options?: {...}): Promise<{list: User[]; total: number; page: number; pageSize: number}>
}