🚀 NestJS + Prisma + BullMQ
现代 Node.js 后端开发三剑客 · 完整教程
📖 适合读者:会 TypeScript + 基础 Node.js,想快速掌握这三个技术栈
为什么需要这三个技术?
假设你用 Express 开发一个用户系统:
1// app.js - 所有代码混在一起 2const express = require('express'); 3const app = express(); 4 5// 用户相关代码... 6// 订单相关代码... 7// 邮件相关代码... 8// 数据库相关代码... 9// ...几百行后...
问题:代码一多就变成"意大利面条",没人敢改。
像厨房一样分工明确:服务员接单、厨师做菜、传菜员上菜。NestJS 把代码组织成模块(Module),每个模块管自己的控制器(接单)、服务(做菜)、数据访问(拿食材)。
操作数据库要写 SQL,太麻烦而且容易出错。Prisma 让你用 JavaScript 对象的方式操作数据库,自动生成 SQL,还能做类型检查。
发邮件、AI 审查这种耗时操作,不能让用户干等。BullMQ 把任务放进队列,后台慢慢处理,用户立刻得到响应。
传统方式(同步)
用户提交订单 → 系统处理(等5秒)→ 返回成功
用户等待,体验差
队列方式(异步)
用户提交订单 → 立即返回 → 后台处理
用户不等待,体验好
一句话理解
NestJS 是 Node.js 的后端框架,它借鉴了 Angular 的设计思想,用装饰器(@Controller、@Injectable)来声明代码的角色,让代码结构清晰、可测试、可维护。
核心概念
| 概念 | 做什么 | 类比 |
|---|---|---|
| @Controller | 接收 HTTP 请求 | 餐厅服务员 |
| @Injectable | 处理业务逻辑 | 厨师 |
| @Module | 打包相关功能 | 厨房分区 |
| @Pipe | 数据验证/转换 | 质检员 |
第一个 Controller
// user.controller.ts // @Controller('users') - 我是 users 模块的控制器 // 访问路径前缀是 /users @Controller('users') export class UsersController { // @Get() - 处理 GET 请求 // 完整路径: GET /users @Get() findAll() { return [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, ]; } // @Get(':id') - :id 是路由参数 // 完整路径: GET /users/123 @Get(':id') findOne(@Param('id') id: string) { return { id, name: '用户' + id }; } }
第一个 Service
// user.service.ts // @Injectable() - 表示此类可以被依赖注入 // NestJS 会自动创建这个类的实例 @Injectable() export class UsersService { // 模拟数据库中的用户 private users = [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' }, ]; findAll() { return this.users; } findById(id: number) { return this.users.find(u => u.id === id); } create(data: { name: string; email: string }) { const newUser = { id: this.users.length + 1, ...data }; this.users.push(newUser); return newUser; } }
依赖注入是什么?
简单说:你不用 `new UserService()`,而是告诉 NestJS "我需要一个 UserService",它自动给你。
// 不用 DI(手动创建) const service = new UserService(); // 用 DI(NestJS 自动注入) @Controller('users') export class UsersController { // 在 constructor 中声明依赖 constructor(private readonly usersService: UsersService) {} findAll() { // 直接用,NestJS 已经注入好了 return this.usersService.findAll(); } }
NestJS 进阶
模块化架构
一个完整的 NestJS 应用由多个 Module 组成,每个 Module 包含自己的 Controller、Service、Repository。
// users.module.ts @Module({ controllers: [UsersController], // 控制器 providers: [UsersService], // 服务(可注入的) exports: [UsersService], // 导出给其他模块用 }) export class UsersModule {} // app.module.ts - 根模块 @Module({ imports: [UsersModule], // 引入子模块 }) export class AppModule {}
中间件与拦截器
Middleware(中间件)
请求到达 Controller 之前执行
用途:日志、鉴权、CORS
Interceptor(拦截器)
响应返回之前执行
用途:统一包装响应、缓存
实战:完整 CRUD 模块
// posts.controller.ts - 完整的 CRUD 控制器 @Controller('posts') export class PostsController { @Post() // POST /posts - 创建 create(@Body() dto: CreatePostDto) { return this.postsService.create(dto); } @Get() // GET /posts - 列表 findAll() { return this.postsService.findAll(); } @Get(':id') // GET /posts/:id - 单个 findOne(@Param('id', ParseIntPipe) id: number) { return this.postsService.findOne(id); } @Patch(':id') // PATCH /posts/:id - 部分更新 update(@Param('id') id: number, @Body() dto: UpdatePostDto) { return this.postsService.update(id, dto); } @Delete(':id') // DELETE /posts/:id - 删除 remove(@Param('id') id: number) { return this.postsService.remove(id); } }
一句话理解
Prisma 是一个数据库工具,它让你用声明式的方式定义数据结构,然后用 TypeScript 语法操作数据库,不用写 SQL。
核心概念
| 概念 | 作用 |
|---|---|
| schema.prisma | 定义数据模型(表结构) |
| PrismaClient | 数据库客户端,帮你执行操作 |
| npx prisma migrate | 把 schema 同步到数据库 |
| npx prisma generate | 生成类型安全的客户端代码 |
定义数据模型
// prisma/schema.prisma // generator - 指定生成什么客户端 generator client { provider = "prisma-client-js" } // datasource - 指定用什么数据库 datasource db { provider = "postgresql" url = env("DATABASE_URL") } // model User - 定义 User 表 // @id = 主键,@default(autoincrement()) = 自动增长 // String + ? = 可选字段 model User { id Int @id @default(autoincrement()) name String email String @unique // 唯一索引,不能重复 age Int?? // ? = 可选字段 createdAt DateTime @default(now()) // relation - 一对多关系 // 一个 User 可以有多篇 Post posts Post[] } model Post { id Int @id @default(autoincrement()) title String content String?? published Boolean @default(false) // 外键关联到 User authorId Int author User @relation(fields: [authorId], references: [id]) }
CRUD 操作
// ===== 创建数据 ===== // 创建单个 const user = await prisma.user.create({ data: { name: '张三', email: 'zhangsan@example.com', age: 25 } }); // 批量创建 const users = await prisma.user.createMany({ data: [ { name: '李四', email: 'lisi@example.com' }, { name: '王五', email: 'wangwu@example.com' } ] });
// ===== 读取数据 ===== // 查所有 const users = await prisma.user.findMany(); // 查单个(按唯一字段) const user = await prisma.user.findUnique({ where: { email: 'zhangsan@example.com' } }); // 条件查询 const adults = await prisma.user.findMany({ where: { age: { gt: 18 } } // gt = greater than }); // 分页 const page = await prisma.user.findMany({ take: 10, // 取 10 条 skip: 20 // 跳过前 20 条 });
// ===== 更新数据 ===== // 更新单个 await prisma.user.update({ where: { id: 1 }, data: { name: '新名字' } }); // 条件批量更新 await prisma.user.updateMany({ where: { age: { lt: 18 } }, // lt = less than data: { status: 'minor' } });
// ===== 删除数据 ===== // 删除单个 await prisma.user.delete({ where: { id: 1 } }); // 批量删除 await prisma.user.deleteMany({ where: { status: 'inactive' } });
Prisma 进阶
关联查询(Include)
// 查询用户的同时,把关联的文章也查出来 const user = await prisma.user.findUnique({ where: { id: 1 }, include: { posts: true // 把这个用户的文章也一起查出来 } }); // user.posts 就是这个用户的所有文章 console.log(user.posts);
事务(Transaction)
保证多个数据库操作要么全部成功,要么全部失败。
// 同时创建用户和用户的文章,要么全成功,要么全失败 const result = await prisma.$transaction([ prisma.user.create({ data: { name: '张三', email: 'zhangsan@example.com' } }), prisma.post.create({ data: { title: '你好', authorId: 1 } }) ]);
Raw SQL(原生查询)
当 Prisma 的查询构建器不够用时,可以写原生 SQL。
// 执行原生 SQL const result = await prisma.$queryRaw<User>` SELECT * FROM users WHERE age > ${minAge} `;
一句话理解
BullMQ 是任务队列,让你能异步执行耗时操作。用户提交任务后立即返回,后台慢慢处理,不阻塞主线程。
核心概念
| 概念 | 类比 | 做什么 |
|---|---|---|
| Queue | 餐厅 | 存放任务的地方 |
| Job | 订单 | 具体要执行的任务 |
| Producer | 顾客 | 把任务放进队列 |
| Worker | 厨师 | 从队列取任务执行 |
三步快速上手
// queue.ts import { Queue } from 'bullmq'; // 创建 'email' 队列,连接 Redis const emailQueue = new Queue('email', { connection: { host: 'localhost', port: 6379 } }); export default emailQueue;
// user.service.ts import emailQueue from './queue'; @Injectable() export class UserService { async register(data: { email: string; name: string }) { // 1. 保存用户(同步操作) const user = await this.prisma.user.create({ data }); // 2. 把发邮件任务加入队列(立即返回,不等待) await emailQueue.add('welcome', { to: user.email, name: user.name }); return user; // 立即返回给用户 } }
// email.processor.ts import { Worker, Job } from 'bullmq'; // 创建 Worker,自动从队列取任务执行 const worker = new Worker('email', async (job: Job) => { // job.data 是 add() 时传的数据 const { to, name } = job.data; // 模拟发邮件 console.log(`发送邮件给 ${to},称呼:${name}`); // 更新进度(0-100) await job.updateProgress(100); return { success: true }; }, { connection: { host: 'localhost', port: 6379 } }); console.log('📧 邮件 Worker 已启动,等待任务...');
流程图
BullMQ 进阶
延时任务
任务不立即执行,而是等一段时间后再处理。
// 5 分钟后再执行(适合:延时提醒、定时任务) await emailQueue.add('reminder', { to: 'user@example.com', message: '该打卡了!' }, { delay: 5 * 60 * 1000 // 5 分钟(毫秒) });
重试机制
任务失败时自动重试,不用担心丢任务。
const worker = new Worker('api', async (job) => { // 模拟可能失败的操作 if (Math.random() > 0.7) { throw new Error('网络错误'); } return { success: true }; }, { connection: { host: 'localhost', port: 6379 }, // 重试配置 limiter: { max: 3, // 最多重试 3 次 duration: 5000 // 每次间隔 5 秒 } });
优先级队列
// 添加任务时指定优先级(数字越小优先级越高) await emailQueue.add('vip-notify', { userId: 1 }, { priority: 1 }); // 高优先 await emailQueue.add('daily-digest', { userId: 2 }, { priority: 5 }); // 普通
重复任务(定时)
// 每小时执行一次 await emailQueue.add( 'report', { type: 'daily' }, { repeat: { every: 60 * 60 * 1000 // 每小时(毫秒) } } );
三者如何配合使用?
一个完整请求的处理流程
// 1. Controller 接收请求 @Post() async submit(@Body() dto: { code: string; language: string }) { // 2. Service 保存到数据库(Prisma) const task = await this.prisma.reviewTask.create({ data: { code: dto.code, language: dto.language } }); // 3. 把任务加入队列(BullMQ),立即返回 await this.reviewQueue.add('review', { taskId: task.id }); // 用户不用等待 AI 审查,立即返回 return { taskId: task.id, status: 'queued' }; }
快速回顾
| 技术 | 解决的问题 | 核心 API |
|---|---|---|
| NestJS | 代码组织、模块化、依赖注入 | @Controller, @Injectable, @Module |
| Prisma | 数据库操作、类型安全 | prisma.user.create(), findMany() |
| BullMQ | 异步任务、后台处理 | queue.add(), new Worker() |
- 搭建一个完整的 NestJS 项目,尝试实现 CRUD
- 用 Prisma 连接真实数据库,做一次完整的数据操作
- 写一个 Worker,处理异步任务
- 组合三者,做一个小应用(如任务管理、博客系统)