Skip to content

集成 Prisma

以 PostgreSQL 为例

安装与初始化

bash
pnpm add -D prisma
pnpm add @prisma/client @prisma/adapter-pg pg
pnpm prisma init --datasource-provider postgresql --output ../src/generated/prisma

prisma 提供 CLI 和迁移能力;@prisma/client 是生成的类型安全客户端;@prisma/adapter-pgpg 用于通过 node-postgres 连接 PostgreSQL。

初始化后主要文件为:

bash
prisma
├── migrations
└── schema.prisma
prisma.config.ts
src
└── generated
    └── prisma

生成目录是构建产物,不应手写修改;是否提交它以项目的构建和部署策略为准。

环境变量与配置

.env

md
DATABASE_URL=postgresql://app_user:app_password@localhost:5432/app_db?schema=public

prisma.config.ts

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'),
  },
})

生产环境不要提交真实连接串。应用应使用独立的数据库业务用户,不使用 PostgreSQL 超级用户。

定义 Schema

prisma/schema.prisma

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

datasource db {
  provider = "postgresql"
}

model User {
  id        String   @id @default(uuid()) @db.Uuid
  name      String   @db.VarChar(64)
  email     String   @unique @db.VarChar(255)
  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)

  @@map("users")
}

Prisma Model 描述应用的数据模型,原生类型属性(如 @db.Uuid)用于控制 PostgreSQL 中的具体列类型。DTO 负责校验外部输入,不要直接将 DTO 或客户端传入字段透传给 Prisma。

生成和执行迁移

开发环境生成并应用迁移:

bash
pnpm prisma migrate dev --name init
pnpm prisma generate

migrate dev 生成 SQL 迁移并在开发数据库执行;每次 schema 变化后都应检查生成的 SQL。生产或预发布环境只执行已提交的迁移:

bash
pnpm prisma migrate deploy

migrate deploy 不生成迁移、不重置数据库,也不检查 schema drift。db push 适合原型验证,不能替代多人协作和生产环境中的迁移历史。

可将课程中反复使用的命令收敛到 package.json,避免手写长命令:

json
{
  "scripts": {
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:format": "prisma format",
    "db:studio": "prisma studio"
  }
}

db:studio 用于本地查看和编辑开发数据;生产数据的角色提升、修复和迁移应通过受控运维流程完成。

封装 PrismaService

将 Prisma Client 注册为 Nest Provider,业务 Service 通过依赖注入使用它;不要在每个业务模块中各自创建客户端实例。

src/prisma/prisma.service.ts

ts
import { Injectable, OnModuleDestroy } from '@nestjs/common'
import { PrismaPg } from '@prisma/adapter-pg'

import { PrismaClient } from '../generated/prisma/client.js'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleDestroy {
  constructor() {
    const adapter = new PrismaPg({
      connectionString: process.env.DATABASE_URL!,
    })

    super({ adapter })
  }

  onModuleDestroy() {
    return this.$disconnect()
  }
}

src/prisma/prisma.module.ts

ts
import { Global, Module } from '@nestjs/common'

import { PrismaService } from './prisma.service'

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

@Global() 可以减少重复导入,但会降低依赖显式性。大型项目可以不使用全局模块,在需要访问数据库的业务模块中显式导入 PrismaModule

在业务服务中使用

ts
import { Injectable, NotFoundException } from '@nestjs/common'

import { PrismaService } from '../prisma/prisma.service'

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany({
      select: {
        id: true,
        name: true,
        email: true,
      },
    })
  }

  async findById(id: string) {
    const user = await this.prisma.user.findUnique({ where: { id } })

    if (!user) throw new NotFoundException('用户不存在')

    return user
  }
}

查询时显式指定 selectincludewhere 和排序条件;避免无边界地加载关联数据。列表接口必须限定分页上限,并关注循环查询导致的 N+1 问题。

事务

独立写操作可使用数组事务;需要根据前一步结果决定下一步的业务用例使用交互式事务。

ts
await this.prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { name: 'vfan', email: 'vfan@example.com' },
  })

  return user
})

事务回调内只使用 tx。事务应尽量短,不要在其中执行网络请求、文件 I/O 或长时间计算。

参考

基于 MIT 许可发布