在之前的文章中,我们介绍了使用 TypeORM 进行数据库操作。本文将介绍另一个强大的 ORM 工具 - Prisma,探讨如何在 NestJS 中集成和使用 Prisma。
Prisma 简介与环境搭建
1. 安装依赖
# 安装 Prisma 相关依赖
npm install prisma @prisma/client
# 初始化 Prisma
npx prisma init
2. 配置数据库连接
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
3. 项目结构
src/
├── prisma/
│ ├── schema.prisma # Prisma 模型定义
│ └── migrations/ # 数据库迁移文件
├── modules/
│ └── users/
│ ├── users.service.ts
│ ├── users.controller.ts
│ └── dto/
└── prisma.service.ts # Prisma 服务
模型设计
1. 定义数据模型
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
2. 生成并应用迁移
# 生成迁移文件
npx prisma migrate dev --name init
# 应用迁移
npx prisma migrate deploy
Prisma 服务集成
1. 创建 Prisma 服务
// src/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
super({
log: ['query', 'info', 'warn', 'error'],
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
async cleanDatabase() {
if (process.env.NODE_ENV === 'test') {
const models = Reflect.ownKeys(this).filter((key) => key[0] !== '_');
return Promise.all(
models.map((modelKey) => this[modelKey].deleteMany()),
);
}
}
}
2. 注册 Prisma 模块
// src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
CRUD 操作实现
1. 用户服务实现
// src/modules/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
import { CreateUserDto, UpdateUserDto } from './dto';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async create(data: CreateUserDto) {
return this.prisma.user.create({
data: {
...data,
profile: {
create: data.profile,
},
},
include: {
profile: true,
},
});
}
async findAll(params: {
skip?: number;
take?: number;
where?: any;
orderBy?: any;
}) {
const { skip, take, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
where,
orderBy,
include: {
profile: true,
posts: true,
},
});
}
async findOne(id: number) {
return this.prisma.user.findUnique({
where: { id },
include: {
profile: true,
posts: {
include: {
tags: true,
},
},
},
});
}
async update(id: number, data: UpdateUserDto) {
return this.prisma.user.update({
where: { id },
data: {
...data,
profile: {
update: data.profile,
},
},
include: {
profile: true,
},
});
}
async remove(id: number) {
return this.prisma.user.delete({
where: { id },
});
}
}
2. 控制器实现
// src/modules/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from './dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll(
@Query('skip') skip?: string,
@Query('take') take?: string,
@Query('orderBy') orderBy?: string,
) {
return this.usersService.findAll({
skip: skip ? Number(skip) : undefined,
take: take ? Number(take) : undefined,
orderBy: orderBy ? JSON.parse(orderBy) : undefined,
});
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(Number(id));
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(Number(id), updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(Number(id));
}
}
高级查询技巧
1. 关系查询
// src/modules/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
@Injectable()
export class PostsService {
constructor(private prisma: PrismaService) {}
async findPostsWithRelations() {
// 嵌套关系查询
return this.prisma.post.findMany({
include: {
author: {
include: {
profile: true,
},
},
tags: true,
},
where: {
published: true,
author: {
profile: {
bio: {
contains: 'developer',
},
},
},
},
});
}
async findPostsByTags(tags: string[]) {
// 多对多关系查询
return this.prisma.post.findMany({
where: {
tags: {
some: {
name: {
in: tags,
},
},
},
},
include: {
tags: true,
author: true,
},
});
}
}
2. 事务处理
// src/modules/posts/posts.service.ts
async createPostWithTags(data: CreatePostDto) {
return this.prisma.$transaction(async (tx) => {
// 创建文章
const post = await tx.post.create({
data: {
title: data.title,
content: data.content,
authorId: data.authorId,
},
});
// 处理标签
const tags = await Promise.all(
data.tags.map((tagName) =>
tx.tag.upsert({
where: { name: tagName },
create: { name: tagName },
update: {},
}),
),
);
// 关联文章和标签
await tx.post.update({
where: { id: post.id },
data: {
tags: {
connect: tags.map((tag) => ({ id: tag.id })),
},
},
});
return post;
});
}
3. 聚合查询
// src/modules/analytics/analytics.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
@Injectable()
export class AnalyticsService {
constructor(private prisma: PrismaService) {}
async getPostStats() {
// 聚合统计
const stats = await this.prisma.post.aggregate({
_count: {
_all: true,
published: true,
},
_avg: {
authorId: true,
},
where: {
published: true,
},
orderBy: {
createdAt: 'desc',
},
});
// 分组统计
const postsByAuthor = await this.prisma.post.groupBy({
by: ['authorId'],
_count: {
_all: true,
},
having: {
_count: {
_all: {
gt: 5,
},
},
},
});
return {
stats,
postsByAuthor,
};
}
}
性能优化
1. 查询优化
// src/modules/posts/posts.service.ts
async findPosts(params: {
skip?: number;
take?: number;
where?: any;
}) {
const { skip, take, where } = params;
// 使用 select 只获取需要的字段
return this.prisma.post.findMany({
skip,
take,
where,
select: {
id: true,
title: true,
author: {
select: {
id: true,
name: true,
},
},
},
});
}
// 批量操作
async publishPosts(postIds: number[]) {
return this.prisma.post.updateMany({
where: {
id: {
in: postIds,
},
},
data: {
published: true,
},
});
}
2. 中间件与扩展
// src/prisma/middleware/logging.middleware.ts
import { PrismaClient } from '@prisma/client';
export function addLoggingMiddleware(prisma: PrismaClient) {
prisma.$use(async (params, next) => {
const before = Date.now();
const result = await next(params);
const after = Date.now();
console.log(`Query ${params.model}.${params.action} took ${after - before}ms`);
return result;
});
}
// 扩展 Prisma Client
import { Prisma } from '@prisma/client';
const xprisma = prisma.$extends({
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`;
},
},
},
},
});
测试
1. 单元测试
// src/modules/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../../prisma.service';
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
import { PrismaClient } from '@prisma/client';
describe('UsersService', () => {
let service: UsersService;
let prisma: DeepMockProxy<PrismaClient>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: PrismaService,
useFactory: () => mockDeep<PrismaClient>(),
},
],
}).compile();
service = module.get<UsersService>(UsersService);
prisma = module.get(PrismaService);
});
it('should create a user', async () => {
const user = {
id: 1,
email: 'test@example.com',
name: 'Test User',
};
prisma.user.create.mockResolvedValue(user);
expect(await service.create({ email: 'test@example.com', name: 'Test User' }))
.toEqual(user);
});
});
2. E2E 测试
// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma.service';
describe('UsersController (e2e)', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
prisma = app.get<PrismaService>(PrismaService);
await app.init();
});
beforeEach(async () => {
await prisma.cleanDatabase();
});
afterAll(async () => {
await app.close();
});
it('/users (POST)', () => {
return request(app.getHttpServer())
.post('/users')
.send({
email: 'test@example.com',
name: 'Test User',
})
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('test@example.com');
});
});
});
写在最后
本文详细介绍了在 NestJS 中使用 Prisma 的各个方面:
- 环境搭建与配置
- 数据模型设计
- CRUD 操作实现
- 高级查询技巧
- 性能优化
- 测试策略
Prisma 作为一个现代化的 ORM 工具,提供了类型安全、直观的 API 和优秀的开发体验。通过本文的学习,你应该能够在 NestJS 项目中熟练使用 Prisma 进行数据库操作。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍