Prisma 集成
本教程共 47 篇 · 第 27 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:在 NestJS 中接入 Prisma,学会定义数据模型、封装 PrismaService、用类型安全的方式做 CRUD 和事务。
上一节学了 TypeORM,这一节看另一个选择——Prisma。
Prisma 跟传统 ORM 的思路不太一样。TypeORM 用装饰器标注类,Prisma 用独立的 schema 文件定义模型;TypeORM 的 API 是面向对象风格的 Repository,Prisma 自动生成一套类型安全的客户端。
简单说:Prisma 把数据库操作变成了”查文档式”的 API 调用,而且类型提示特别强。你在 VS Code 里写代码,点几下就能知道有哪些方法、参数是什么类型。
安装和初始化
先装依赖:
npm install @prisma/client
npm install prisma --save-dev
@prisma/client 是运行时用的客户端,prisma 是开发时的 CLI 工具。
然后初始化:
npx prisma init
这条命令会生成两个东西:
prisma/
schema.prisma # 数据模型定义
.env # 数据库连接字符串
配置数据库连接
.env 文件里填数据库地址:
DATABASE_URL="mysql://root:password@localhost:3306/mydb"
不同数据库的 URL 格式不一样:
# PostgreSQL
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
# MySQL
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
# SQLite
DATABASE_URL="file:./dev.db"
在 schema.prisma 里配置数据源:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
provider 选你的数据库类型,url 从环境变量读取。
NotePrisma 较新版本默认生成 ESM 模块。NestJS 用的是 CommonJS,所以需要在 generator 里加
moduleFormat = "cjs",不然 import 会报错。
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
定义数据模型
Prisma 的模型定义不用 TypeScript 装饰器,而是在 schema.prisma 文件里用 Prisma 自己的 DSL(领域特定语言)。
基本模型
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
语法很直观:模型名、字段名、字段类型,后面跟 @ 开头的属性。
常用属性
| 属性 | 含义 |
|---|---|
@id | 主键 |
@default(autoincrement()) | 自增默认值 |
@default(uuid()) | UUID 默认值 |
@default(now()) | 当前时间 |
@unique | 唯一约束 |
@updatedAt | 自动更新时间戳 |
@map("column_name") | 映射到不同的数据库列名 |
@db.VarChar(100) | 指定数据库列类型 |
String? 里的问号表示可选字段,对应数据库的 NULL。没有问号就是必填字段。
关系定义
Prisma 的关系用 @relation 声明,跟 TypeORM 的装饰器风格不同,但表达的意思一样:
- 一对一:一方用
@unique约束外键 - 一对多:用
@relation在两边互相关联 - 多对多:两边都声明数组类型字段,Prisma 自动建中间表
// 一对多:User 有多篇 Post
model User {
posts Post[]
}
model Post {
author User @relation(fields: [authorId], references: [id])
authorId Int
}
fields 是当前表的外键字段,references 是关联表的主键字段。
TipPrisma 的关系必须两边都声明。
User里有posts Post[],Post里也要有对User的引用。这跟 TypeORM 一样,关系是双向的。
生成客户端
模型写好后,运行迁移来建表并生成客户端:
npx prisma migrate dev --name init
这条命令做了三件事:
- 对比 schema 和数据库现状,生成 SQL 迁移文件
- 执行迁移,把表建好
- 生成 Prisma Client 代码
以后每次改了 schema,都要重新跑这条命令。
如果只想生成客户端(比如 CI/CD 环境),用:
npx prisma generate
创建 PrismaService
在 NestJS 里使用 Prisma,需要一个 Service 来管理数据库连接。这个 Service 继承 PrismaClient,同时实现 NestJS 的生命周期钩子:
// prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from './generated/prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
OnModuleInit 在模块初始化时调用,这时候连上数据库。OnModuleDestroy 在应用关闭时调用,断开连接。这样数据库连接的生命周期就跟 NestJS 应用绑定了。
封装成全局模块
PrismaService 基本上每个模块都要用,所以做成全局模块比较方便:
// prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
@Global() 让它变成全局模块,在 AppModule 里 import 一次就行,其他模块不用重复导入。
// app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
@Module({
imports: [PrismaModule],
})
export class AppModule {}
CRUD 操作
有了 PrismaService,在任何 Service 里注入就能用。Prisma Client 的 API 是按模型分的,比如 this.prisma.user 就是操作 User 表的。
创建
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async create(createUserDto: CreateUserDto) {
return this.prisma.user.create({
data: createUserDto,
});
}
// 创建时同时创建关联数据
async createWithProfile(userData: any, profileData: any) {
return this.prisma.user.create({
data: {
...userData,
profile: {
create: profileData,
},
},
include: {
profile: true,
},
});
}
}
create 的 data 字段就是要写入的数据。如果要同时创建关联数据,在 data 里嵌套 create 就行。Prisma 会自动处理外键关系。
查询
// 查所有
async findAll() {
return this.prisma.user.findMany();
}
// 按 ID 查,带关联数据
async findOne(id: number) {
return this.prisma.user.findUnique({
where: { id },
include: {
posts: true,
profile: true,
},
});
}
// 按邮箱查
async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
});
}
// 条件查询 + 排序
async findActive() {
return this.prisma.user.findMany({
where: { isActive: true },
orderBy: { createdAt: 'desc' },
});
}
findUnique 用于有唯一约束的字段(id、email 等),返回单条或 null。findMany 返回数组,支持各种过滤和排序。
更新
async update(id: number, updateUserDto: UpdateUserDto) {
return this.prisma.user.update({
where: { id },
data: updateUserDto,
});
}
// upsert:有就更新,没有就创建
async upsert(id: number, data: any) {
return this.prisma.user.upsert({
where: { id },
update: data,
create: { id, ...data },
});
}
upsert 是个很实用的方法,不用先查再判断,一条命令搞定”有就更新、没有就创建”。
删除
async remove(id: number) {
return this.prisma.user.delete({
where: { id },
});
}
// 批量删除
async deleteMany(ids: number[]) {
return this.prisma.user.deleteMany({
where: {
id: { in: ids },
},
});
}
查询选项
Prisma 的查询选项设计得很清晰,常用的有这几个:
select - 选择字段
const user = await this.prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
name: true,
email: true,
},
});
只返回指定的字段。true 表示要这个字段,false 或者不写就不要。
include - 包含关联
const user = await this.prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
include: {
tags: true, // 嵌套 include
},
},
profile: true,
},
});
include 可以嵌套。查用户的时候连带查出文章,查文章的时候再连带查出标签。
Tip
select和include不能同时用在同一级。要么选字段,要么包含关联,二选一。如果需要在 include 关联的同时选字段,可以在 include 内部用 select。
where - 条件过滤
const users = await this.prisma.user.findMany({
where: {
AND: [
{ isActive: true },
{
OR: [
{ name: { contains: 'John' } },
{ email: { endsWith: '@example.com' } },
],
},
],
},
});
Prisma 的条件过滤是对象式的,不用写 SQL 字符串。contains、endsWith、startsWith 这些对应 SQL 的 LIKE。AND、OR、NOT 用来组合条件。
分页
// 偏移分页
const users = await this.prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
});
// 游标分页(性能更好)
const users = await this.prisma.user.findMany({
cursor: { id: lastId },
take: limit,
});
skip + take 是传统的偏移分页。cursor 是游标分页,适合数据量大的场景——不用扫前面所有行,直接从上次的位置继续。
排序
const users = await this.prisma.user.findMany({
orderBy: [
{ createdAt: 'desc' },
{ name: 'asc' },
],
});
支持多字段排序,传数组就行。
事务处理
Prisma 提供了两种事务方式。
批量事务
把多个操作打包成一个数组,一次性执行:
const result = await this.prisma.$transaction([
this.prisma.user.create({ data: userData }),
this.prisma.profile.create({ data: profileData }),
]);
简单直接,适合几个独立操作的组合。但这几个操作之间没有依赖关系——不能拿第一个操作的结果去决定第二个操作。
交互式事务
需要操作之间有依赖时,用回调式的写法:
const result = await this.prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
await tx.profile.create({
data: { ...profileData, userId: user.id },
});
return user;
});
回调函数里的 tx 是一个事务上下文,用法跟 prisma 一样。如果回调里抛出异常,整个事务自动回滚。
Note交互式事务有默认的超时时间(默认 5 秒)。如果事务执行时间较长,可以通过
maxWait和timeout参数调整:this.prisma.$transaction(async (tx) => { ... }, { maxWait: 10000, // 最长等待 10 秒 timeout: 30000, // 事务超时 30 秒 });
原始 SQL 查询
Prisma 的 API 能覆盖大部分场景,但偶尔需要写原生 SQL:
// 查询
const result = await this.prisma.$queryRaw`
SELECT * FROM users WHERE id = ${id}
`;
// 执行(不返回数据)
await this.prisma.$executeRaw`
UPDATE users SET isActive = false WHERE id = ${id}
`;
注意用的是模板字符串标签语法(tagged template),不是普通字符串拼接。这样 Prisma 会自动处理参数转义,防止 SQL 注入。
数据库迁移
Prisma 的迁移工具叫 Prisma Migrate,用起来很简单。
开发环境
# 创建并执行迁移
npx prisma migrate dev --name add_new_field
# 重置数据库(清空所有数据)
npx prisma migrate reset
migrate dev 会对比 schema 和数据库现状,生成 SQL 文件并执行。每次改了 schema 都要跑一次。
生产环境
# 只执行待应用的迁移
npx prisma migrate deploy
migrate deploy 不会生成新迁移,只会把还没执行的迁移跑一遍。适合 CI/CD 流水线。
Prisma Studio
Prisma 自带一个数据库可视化工具:
npx prisma studio
会在浏览器打开一个界面,可以直接查看和编辑数据。开发调试的时候很好用。
错误处理
Prisma 的错误有明确的错误码,可以针对性地处理:
import { Injectable, ConflictException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async create(data: CreateUserDto) {
try {
return await this.prisma.user.create({ data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// P2002 是唯一约束冲突
if (error.code === 'P2002') {
throw new ConflictException('邮箱已存在');
}
}
throw error;
}
}
}
常见的错误码:
| 错误码 | 含义 |
|---|---|
P2002 | 唯一约束冲突 |
P2003 | 外键约束失败 |
P2025 | 记录不存在 |
Tip建议封装一个 Prisma 异常过滤器,统一处理这些错误。不然每个 Service 方法里都要写 try-catch,代码很冗余。
测试时的 Mock
Prisma 的类型系统让 mock 变得很方便。因为 PrismaService 就是 PrismaClient 的子类,直接 mock 整个 Service 就行:
const mockPrismaService = {
user: {
findMany: jest.fn().mockResolvedValue([]),
findUnique: jest.fn().mockResolvedValue(null),
create: jest.fn().mockResolvedValue({}),
update: jest.fn().mockResolvedValue({}),
delete: jest.fn().mockResolvedValue({}),
},
};
@Module({
providers: [
UsersService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
})
export class UsersModule {}
Prisma vs TypeORM 怎么选
两者各有优势,没有绝对的好坏:
| 维度 | Prisma | TypeORM |
|---|---|---|
| 类型安全 | 自动生成,非常强 | 依赖装饰器,一般 |
| 模型定义 | 独立 schema 文件 | TypeScript 装饰器 |
| 迁移工具 | 内置,体验好 | 内置,但配置复杂 |
| 查询方式 | 对象式 API | Repository + QueryBuilder |
| 数据库支持 | PG/MySQL/SQLite/MongoDB 等 | 几乎所有主流数据库 |
| 学习曲线 | 低,API 直观 | 中等,概念多 |
| 社区生态 | 新,发展快 | 老,资料多 |
如果你是新项目、团队对 TypeScript 比较熟,Prisma 的开发体验通常更好。如果项目已有 TypeORM 基础或者需要更灵活的数据库操作,TypeORM 也是很好的选择。
小结
Prisma 跟 NestJS 的集成核心就三步:定义 schema 模型、封装 PrismaService、注入使用。
关键知识点回顾:
- Prisma 用独立的 schema 文件定义模型,不用 TypeScript 装饰器
- PrismaService 继承 PrismaClient,通过生命周期钩子管理连接
- 做成
@Global()模块,全局可用 - CRUD 操作是类型安全的,IDE 提示非常完善
- 事务分批量式和交互式两种
- 迁移用
prisma migrate dev(开发)和prisma migrate deploy(生产) - 别忘了设
moduleFormat = "cjs",不然跟 NestJS 的 CommonJS 不兼容
下一章咱们看看 NoSQL 方向——MongoDB 集成。