首页 / NestJS 入门教程 / Prisma 集成

NestJS 入门教程

Prisma 集成

本教程共 47 篇 · 第 27 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJSPrismaORM数据库类型安全迁移

本节目标:在 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 从环境变量读取。

Note

Prisma 较新版本默认生成 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 是关联表的主键字段。

Tip

Prisma 的关系必须两边都声明。User 里有 posts Post[]Post 里也要有对 User 的引用。这跟 TypeORM 一样,关系是双向的。

生成客户端

模型写好后,运行迁移来建表并生成客户端:

npx prisma migrate dev --name init

这条命令做了三件事:

  1. 对比 schema 和数据库现状,生成 SQL 迁移文件
  2. 执行迁移,把表建好
  3. 生成 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,
      },
    });
  }
}

createdata 字段就是要写入的数据。如果要同时创建关联数据,在 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

selectinclude 不能同时用在同一级。要么选字段,要么包含关联,二选一。如果需要在 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 字符串。containsendsWithstartsWith 这些对应 SQL 的 LIKEANDORNOT 用来组合条件。

分页

// 偏移分页
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 秒)。如果事务执行时间较长,可以通过 maxWaittimeout 参数调整:

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 怎么选

两者各有优势,没有绝对的好坏:

维度PrismaTypeORM
类型安全自动生成,非常强依赖装饰器,一般
模型定义独立 schema 文件TypeScript 装饰器
迁移工具内置,体验好内置,但配置复杂
查询方式对象式 APIRepository + 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 集成。