Prisma 入门指南:像写代码一样操作数据库
如果你受够了写 SQL 语句的繁琐,厌倦了传统 ORM 的类型丢失,Prisma 可能正是你在寻找的工具。这篇教程将带你从零掌握 Prisma,用更优雅的方式与数据库交互。
本文档适用于 Prisma 7+ 版本
前言
传统的数据库操作方式总是让人割裂:写好了 TypeScript 类型定义,数据库表结构又是另一套描述;改一个字段,要同时维护三处代码;最离谱的是,写错了字段名要等到运行时才能发现。
Prisma 试图解决这些问题。它用一份声明式的 schema.prisma 文件定义数据模型,然后自动生成 TypeScript 类型的数据库客户端。这意味着:当你改 Schema 时,类型会自动更新;当你写查询时,IDE 会告诉你字段是否正确。
这篇教程按学习顺序展开,从环境搭建到实战 CRUD,每个章节都有可运行的代码示例。建议你跟着敲一遍,效果更好。
目录
- 环境搭建:5 分钟跑起来
- 模型定义:Prisma 的核心
- 数据迁移:安全地变更数据库结构
- 客户端:代码里怎么用
- CRUD 实战:增删改查
- 关系操作:处理表关联
- 跨表查询:多表联合检索
- 事务调用:原子操作与一致性
- 种子数据:快速填充测试数据
- Prisma 优势与安全性
- 进阶技巧:错误处理与性能
1. 环境搭建:5 分钟跑起来
安装
首先创建一个新项目,全程只需要几个命令:
mkdir my-prisma-app && cd my-prisma-app
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npx prisma initprisma(开发依赖):命令行工具,只负责生成客户端、执行迁移等开发阶段的工作@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(绝对路径):
DATABASE_URL="file:./sqlite.db"Tip:SQLite 数据库文件会在首次迁移时自动创建。
./sqlite.db表示相对于项目根目录。
初始化 Schema
schema.prisma 定义数据模型,prisma.config.ts 管理连接配置:
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "./generated"
}
datasource db {
provider = "sqlite"
relationMode = "prisma"
}// 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/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 | 日期时间 | 创建时间、更新时间 |
Json | JSON 对象 | 灵活的结构化数据 |
Bytes | 二进制 | 文件、图片 |
Decimal | 精确小数 | 货币、精确计算 |
字段修饰符
修饰符控制字段的行为,就像给字段加了一层能力:
| 修饰符 | 说明 | 示例 |
|---|---|---|
? | 可选字段,允许 null | name 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 声明。常见的有三种:
一对多(一个用户有多篇文章):
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 不是可选的
}一对一(一个用户有一份资料):
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 // 一对一需要外键唯一
}多对多(一篇文章有多个标签):
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")。
配置示例
// PostgreSQL/MySQL 物理外键模式(使用数据库外键)
datasource db {
provider = "postgresql"
relationMode = "foreignKeys"
}
// SQLite 或需要逻辑外键时
datasource db {
provider = "sqlite"
relationMode = "prisma"
}两种模式对比:
| 特性 | foreignKeys | prisma |
|---|---|---|
| 数据库外键 | ✅ 创建 | ❌ 不创建 |
| 数据一致性 | 数据库级约束 | 应用级约束 |
| 迁移 | 支持 | 不需要迁移 |
| 写入性能 | 可能有约束开销 | 更快 |
| 跨表查询 | SQL JOIN | Prisma Client |
Tip:关系模式与
provider无关,PostgreSQL/MySQL 也可以使用prisma模式。SQLite 由于不支持外键约束,只能使用prisma模式。
枚举类型
model User {
id Int @id @default(autoincrement())
name String
role Role @default(USER)
}
enum Role {
USER
ADMIN
MODERATOR
}Tip:SQLite 原生不支持枚举,Prisma 会将其存储为 Text 并在客户端做类型校验。
软删除支持
实际项目中,推荐使用软删除保留数据:
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:
npx prisma migrate dev --name init--name 参数是本次迁移的描述,比如 --name add_user_table、--name add_avatar_field。这条命令会:
- 在
prisma/migrations/下创建迁移文件夹 - 执行 SQL 到数据库
- 自动运行
prisma generate
快速原型用 db push:
npx prisma db push适合不想管理迁移文件的时候,但不推荐在生产环境使用。
生产环境用 migrate deploy:
npx prisma migrate deploy它只执行已有的迁移文件,不会生成新的。确保迁移文件已经提交到 Git。
分阶段开发流程
开发流程建议分为三个阶段,避免迁移文件碎片化:
阶段一:开发调试(频繁调整 Schema)
npx prisma db push- 直接同步数据库结构,不生成迁移文件
- 适合频繁修改字段、测试功能阶段
- 开发完成后删除本地数据库重新来过也很方便
阶段二:提测前合并(准备发布)
npx prisma migrate dev --create-only --name init- 生成一个完整的迁移文件
- 将之前零散的调整合并为一次迁移
- 提交到 Git 后再提测
阶段三:正式开发(功能稳定后)
npx prisma migrate dev --name add_user_avatar- 每次 Schema 变更生成一个迁移文件
- 自动执行迁移 + 生成类型
- 适合功能开发完成、进入维护阶段
迁移文件管理原则
| 原则 | 说明 |
|---|---|
开发调试用 db push | 不留痕迹,随时重来 |
| 提测/发布前合并 | 用 --create-only 生成单一迁移 |
| 上线后谨慎变更 | 每次变更加密记录,保留历史 |
查看迁移状态
npx prisma migrate status输出会显示哪些迁移已应用、哪些待执行。
上线部署流程
生产环境部署遵循以下步骤:
第一步:准备迁移文件
确保所有迁移文件已提交到 Git,并在生产服务器拉取最新代码:
git pull origin main第二步:执行数据库迁移
npx prisma migrate deploy- 只执行已有迁移,不会生成新文件
- 如果迁移已在生产数据库执行过,会自动跳过
- 建议在应用启动前或发布的早期阶段执行
第三步:生成客户端类型
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. 客户端:代码里怎么用
回答安装时埋下的伏笔
第一章我们注意到一个细节:
npm install prisma --save-devprisma 装在了 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 命令:
npx prisma generate这条命令做了三件事:
- 读取
schema.prisma的模型定义 - 根据模型生成对应的 TypeScript 类型和 CRUD 方法
- 输出到 schema 中
output指定的目录(默认为./generated)
生成的内容包含:
- 每个模型的 TypeScript 类型定义
PrismaClient类及其所有方法- 自动推导的智能提示类型
每次修改 Schema 后都要重新 generate,否则客户端代码与数据库结构会不一致。
初始化 PrismaClient
生成客户端后,就可以在代码里使用了。推荐在 src/prisma/index.ts 中创建统一出口:
import { PrismaClient } from '../../prisma/generated'
// 直接实例化使用
const prisma = new PrismaClient()
export default prisma注意:实际项目中,生成的客户端路径取决于 schema 中
output的配置。本项目配置为./generated,所以导入路径是../../prisma/generated。
单例模式(开发环境推荐)
开发模式下修改代码会触发热更新,Node.js 会重新执行模块。如果每次都 new PrismaClient(),会创建多个数据库连接,最终耗尽连接池。用单例模式可以避免这个问题:
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其他文件使用
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(创建)
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(查询)
// 查询单条(按主键)
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' },
},
})常用查询条件:
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(更新)
// 更新单条
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' },
})Tip:
increment、decrement、multiply、divide是原子操作,在并发场景下比先读后写更安全。
Delete(删除)
// 删除单条(硬删除)
await prisma.user.delete({
where: { id: 1 },
})
// 批量删除(硬删除)
await prisma.user.deleteMany({
where: { age: { lt: 18 } },
})
// 删除所有(慎用!)
await prisma.user.deleteMany({})6. 关系操作:处理表关联
嵌套查询(include)
查询用户时同时获取关联的文章:
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,
},
},
},
})嵌套创建
创建用户时同时创建文章:
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)
// 使用 createMany 批量创建文章
const user = await prisma.user.update({
where: { id: 1 },
data: {
posts: {
createMany: {
data: [
{ title: '标题', authorId: 1 },
],
},
},
},
include: { posts: true },
})统计关联数量
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 允许你用链式调用的方式逐步深入关联表:
// 从 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 条件中直接引用关联表的字段:
// 查询所有包含已发布文章的用户
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 |
深度嵌套查询
可以多层嵌套,查询更深层的关系:
// 查询:获取用户 -> 该用户的文章 -> 文章的作者 -> 作者的文章
const result = await prisma.user.findMany({
where: { id: 1 },
include: {
posts: {
include: {
user: {
include: {
posts: {
select: { id: true, title: true },
},
},
},
},
},
},
})聚合与分组
// 统计每个用户的文章数量
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',
},
},
})跨表更新
通过 update 的 connect 和 disconnect 操作关联关系:
// 将已有的文章关联到用户
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 在关联存在时连接,不存在时创建:
// 查询或创建分类,然后关联到文章
const post = await prisma.post.update({
where: { id: 1 },
data: {
categories: {
connectOrCreate: {
where: { name: '技术' },
create: { name: '技术', slug: 'tech' },
},
},
},
})8. 事务调用:原子操作与一致性
为什么需要事务
事务确保一组数据库操作要么全部成功,要么全部失败。当业务逻辑涉及多个表的多步操作时,事务是保证数据一致性的关键。
典型场景:
- 转账:从账户 A 扣款,向账户 B 存款,必须同时成功或同时失败
- 创建订单:创建订单记录、扣减库存、记录日志,任何一步失败都应该回滚
- 注册用户:创建用户记录、初始化用户配置、发送欢迎邮件
Prisma 事务的三种方式
方式一:交互式事务(推荐)
$transaction 接收一个回调函数,在事务上下文中执行所有操作:
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,自动打包成事务:
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)
await prisma.$transaction(async (tx) => {
// ... 操作
}, {
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
})可选的隔离级别:
ReadCommitted(默认)Serializable(最强隔离,防止所有并发问题)
注意:不是所有数据库都支持所有隔离级别。SQLite 只支持
Serializable。
嵌套事务(Savepoint)
在交互式事务中可以创建保存点,实现部分回滚:
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' } })
})实际业务场景示例
场景一:转账
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 }
})
}场景二:创建订单
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 代替循环单条插入 |
常见错误处理
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 中添加:
"prisma": {
"seed": "tsx prisma/seed.ts"
}需要安装 tsx(已安装):
npm install -D tsx编写 seed 脚本
创建 prisma/seed.ts:
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
npx prisma db seedTip:
migrate 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 所有查询都使用参数化查询,不需要手动转义用户输入:
// 安全的写法 - 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. 进阶技巧:错误处理与性能
错误处理
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 只查需要的字段 |
| 分页 | 用 take 和 skip 避免大结果集 |
| 索引 | 对高频查询字段加 @index |
model User {
id Int @id @default(autoincrement())
email String @unique
name String
age Int
@@index([age]) // 单字段索引
@@index([name, age]) // 复合索引
}原始 SQL
当 Prisma 的查询构建器不够用时,可以用 $queryRaw:
// 查询(返回类型需手动指定)
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 注入。
连接池配置
const prisma = new PrismaClient({
datasources: {
db: {
url: env("DATABASE_URL"),
},
},
log: ['query', 'info', 'warn', 'error'],
})总结
这篇教程覆盖了 Prisma 的核心知识点:
- Schema 是核心:一份文件定义模型,生成类型安全的客户端
- 模型定义:掌握字段类型、修饰符、关系声明
- 迁移:开发用
migrate dev,生产用migrate deploy - 客户端:正确导入生成的客户端,避免连接耗尽
- CRUD:Create / Read / Update / Delete 的标准写法
- 关系操作:
include嵌套查询、createMany批量创建 - 跨表查询:Fluent API、隐式联表、聚合分组
- 事务调用:原子操作保证数据一致性
- 种子数据:测试数据快速填充
- Prisma 优势:类型安全、优雅 API、迁移系统、开发体验
- 进阶技巧:错误处理、性能优化、原始 SQL
下一步
- 查阅 Prisma 官方文档,深入了解高级特性
- 使用
npx prisma studio启动可视化数据库管理界面 - 尝试在实际项目中应用 Prisma,体验类型安全带来的开发效率提升
- 探索以下官方资源:
- Prisma SQLite 快速开始 - 本教程对应的官方文档
- Prisma Client API 参考 - 所有 CRUD 方法详解
- Prisma Migrate 文档 - 迁移系统详解
- Prisma Schema 参考 - 所有字段类型和修饰符
有问题或发现教程中的疏漏,欢迎交流讨论。
附录
常用命令速查
| 命令 | 说明 |
|---|---|
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 类型 | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
String | TEXT | VARCHAR | VARCHAR |
Int | INTEGER | INTEGER | INT |
Float | REAL | DOUBLE | DOUBLE |
Boolean | INTEGER | BOOLEAN | BOOLEAN |
DateTime | TEXT | TIMESTAMP | DATETIME |
Json | TEXT | JSONB | JSON |
注意: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服务层设计参考
实际项目中推荐的服务层封装模式:
// 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}>
}