NestJS Prisma集成 #

Prisma简介 #

Prisma是一个现代数据库工具包,包含Prisma Client(自动生成的类型安全查询构建器)、Prisma Migrate(数据库迁移工具)和Prisma Studio(数据库GUI)。

安装依赖 #

bash
npm install prisma --save-dev
npm install @prisma/client

初始化Prisma #

bash
npx prisma init

生成的文件结构:

text
prisma/
└── schema.prisma    # 数据库模型定义
.env                 # 环境变量

配置数据库 #

schema.prisma #

prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// PostgreSQL
// datasource db {
//   provider = "postgresql"
//   url      = env("DATABASE_URL")
// }

// SQLite
// datasource db {
//   provider = "sqlite"
//   url      = "file:./dev.db"
// }

.env #

env
DATABASE_URL="mysql://user:password@localhost:3306/mydb"

定义模型 #

基本模型 #

prisma
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[]
}

字段类型 #

类型 说明
String 字符串
Int 整数
Float 浮点数
Boolean 布尔值
DateTime 日期时间
Json JSON数据
Bytes 二进制数据
Decimal 高精度小数

属性装饰器 #

属性 说明
@id 主键
@default(autoincrement()) 自增
@default(uuid()) UUID
@default(now()) 当前时间
@unique 唯一约束
@updatedAt 自动更新时间
@map("column_name") 映射列名
@db.VarChar(100) 数据库类型

Prisma服务 #

创建Prisma服务 #

typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

注册Prisma模块 #

typescript
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

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

CRUD操作 #

创建 #

typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';

@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,
      },
    });
  }
}

查询 #

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

  async findAll() {
    return this.prisma.user.findMany();
  }

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

更新 #

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

  async update(id: number, updateUserDto: UpdateUserDto) {
    return this.prisma.user.update({
      where: { id },
      data: updateUserDto,
    });
  }

  async updateByEmail(email: string, data: any) {
    return this.prisma.user.update({
      where: { email },
      data,
    });
  }

  async upsert(id: number, data: any) {
    return this.prisma.user.upsert({
      where: { id },
      update: data,
      create: { id, ...data },
    });
  }
}

删除 #

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

  async remove(id: number) {
    return this.prisma.user.delete({
      where: { id },
    });
  }

  async deleteMany(ids: number[]) {
    return this.prisma.user.deleteMany({
      where: {
        id: { in: ids },
      },
    });
  }
}

查询选项 #

select #

选择特定字段:

typescript
const user = await this.prisma.user.findUnique({
  where: { id: 1 },
  select: {
    id: true,
    name: true,
    email: true,
  },
});

include #

包含关联数据:

typescript
const user = await this.prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        tags: true,
      },
    },
    profile: true,
  },
});

where #

条件查询:

typescript
const users = await this.prisma.user.findMany({
  where: {
    AND: [
      { isActive: true },
      {
        OR: [
          { name: { contains: 'John' } },
          { email: { endsWith: '@example.com' } },
        ],
      },
    ],
  },
});

orderBy #

排序:

typescript
const users = await this.prisma.user.findMany({
  orderBy: [
    { createdAt: 'desc' },
    { name: 'asc' },
  ],
});

分页 #

typescript
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,
});

事务处理 #

批量操作 #

typescript
const result = await this.prisma.$transaction([
  this.prisma.user.create({ data: userData }),
  this.prisma.profile.create({ data: profileData }),
]);

交互式事务 #

typescript
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;
});

原始查询 #

typescript
// 原始SQL查询
const result = await this.prisma.$queryRaw`
  SELECT * FROM users WHERE id = ${id}
`;

// 执行原始SQL
await this.prisma.$executeRaw`
  UPDATE users SET isActive = false WHERE id = ${id}
`;

数据库迁移 #

创建迁移 #

bash
npx prisma migrate dev --name init

应用迁移 #

bash
npx prisma migrate deploy

重置数据库 #

bash
npx prisma migrate reset

Prisma Studio #

启动数据库GUI:

bash
npx prisma studio

生成客户端 #

bash
npx prisma generate

最佳实践 #

1. 使用DTO #

typescript
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

2. 错误处理 #

typescript
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';

@Injectable()
export class UsersService {
  async create(data: CreateUserDto) {
    try {
      return await this.prisma.user.create({ data });
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        if (error.code === 'P2002') {
          throw new ConflictException('Email already exists');
        }
      }
      throw error;
    }
  }
}

3. 软删除 #

prisma
model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String
  deletedAt DateTime?
  isActive  Boolean   @default(true)

  @@filter(deletedAt == null)
}

4. 分页封装 #

typescript
export class PaginationService {
  async paginate(
    model: any,
    page: number,
    limit: number,
    where?: any,
  ) {
    const [data, total] = await Promise.all([
      model.findMany({
        where,
        skip: (page - 1) * limit,
        take: limit,
      }),
      model.count({ where }),
    ]);

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }
}

总结 #

本章学习了NestJS与Prisma的集成:

  • Prisma配置和模型定义
  • Prisma服务创建
  • CRUD操作
  • 查询选项
  • 事务处理
  • 数据库迁移

接下来,让我们学习 MongoDB集成

最后更新:2026-03-28