NestJS + Prisma:我用 ORM 写 SQL,老板以为我雇了个 DBA

5 阅读4分钟

从 schema 设计到 JWT 登录,一个 TypeScript 后端的“降维打击”
技术栈:NestJS 10 + TypeScript + Prisma 6 + PostgreSQL + JWT + class-validator


🧠 开篇:当后端开始写“类”,DBA 开始慌了

“你这数据库关系这么复杂——用户、文章、评论、标签、点赞、头像、文件……至少要三天建表吧?”
“不用,我写个 schema.prisma,一条命令搞定。”
“……你是不是偷偷请了 DBA?”

在 NestJS + Prisma 的世界里,数据库不再是黑盒,而是 TypeScript 类的延伸
User 表?就是一个 model User {}
多对多标签?PostTag 联结表自动生成。
连外键约束、索引、字段映射,都能用装饰器搞定。

今天,就带你看看这套“现代化后端流水线”是如何让 CRUD 变成艺术的。


🗃️ 一、Prisma Schema:数据库的“TypeScript 设计稿”

传统方式:先建库 → 再建表 → 再写 Model → 再写 Migration。
Prisma 方式:先写 schema,再一键生成一切

// prisma/schema.prisma
model Post {
  id      Int    @id @default(autoincrement())
  title   String @db.VarChar(255)
  content String? @db.Text
  user    User?  @relation(fields: [userId], references: [id])
  userId  Int?
  tags    PostTag[]
  likes   UserLikesPost[]
  @@index([userId])
  @@map("posts")
}

这段代码,既是文档,又是代码,还是数据库蓝图。

  • @relation 自动处理外键
  • @@map("posts") 映射真实表名(避免复数歧义)
  • @db.VarChar(255) 精确控制字段类型
  • @@index([userId]) 自动生成数据库索引

为什么 Prisma 比 TypeORM 更香?

对比项TypeORMPrisma
查询语法链式方法函数式 + include
类型推导强(自动生成 TS 类型)
迁移手动或 auto完全可控
性能中等极快(查询构建器优化)
学习曲线陡峭平缓

💡 真实案例:我们有一个“获取文章详情 + 作者 + 标签 + 点赞数”的接口。
用 Prisma 一行 include 搞定:

this.prisma.post.findUnique({
  where: { id },
  include: {
    user: { select: { name: true } },
    tags: { include: { tag: true } },
    likes: { select: { userId: true } }
  }
});

而 TypeORM 需要写 3 个 left join + 手动去重——Prisma 把复杂留给自己,把简单留给开发者

初始化流程(三步走)

  1. 创建数据库

    CREATE DATABASE notes_ai WITH OWNER=postgres ENCODING='UTF8';
    
  2. 初始化 Prisma

    npx prisma init
    # 生成 .env + prisma/schema.prisma
    
  3. 迁移 & 生成 Client

    npx prisma migrate dev --name init-user
    npx prisma generate
    

从此,@prisma/client 会根据 schema 自动生成类型安全的查询 API——连字段拼错都会被 TS 编译器拦下!


🧪 二、DTO + ValidationPipe:参数校验,优雅到骨子里

前端传个 page=abc?直接 400 报错。
多传了个 hackField?自动过滤(whitelist: true)。
少传必填字段?立刻拦截(forbidNonWhitelisted: true)。

这一切,靠的是 class-validator + class-transformer

// dto/post-query.dto.ts
import { IsOptional, IsInt, Min, Max, Type } from 'class-validator';

export class PostQueryDto {
  @IsOptional()
  @Type(() => Number)        // 自动 string → number
  @IsInt({ message: 'page 必须是整数' })
  @Min(1, { message: 'page 至少为 1' })
  page?: number = 1;

  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Max(100, { message: 'limit 不能超过 100' })
  limit?: number = 10;
}

main.ts 中启用全局管道:

// main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,        // 自动转换类型
      whitelist: true,        // 删除非 DTO 定义的属性
      forbidNonWhitelisted: true, // 非白名单字段直接报错
      validationError: { target: false } // 隐藏内部类名,保护隐私
    })
  );
  await app.listen(3000);
}

从此,控制器方法签名干净如诗

// posts.controller.ts
@Get()
findAll(@Query() query: PostQueryDto) {
  // query.page 已是 number,无需 parseInt!
  return this.postsService.findAll(query);
}

🤯 反常识:最好的错误处理,是在请求进入业务逻辑前就拦截掉。
用户看到的不是“服务器错误”,而是“page 必须是整数”——这就是专业


🔌 三、PrismaService:依赖注入的极致优雅

NestJS 的依赖注入系统,让数据库客户端像自来水一样随处可用:

// prisma/prisma.service.ts
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/prisma.module.ts
import { Global, Module } from '@nestjs/common';

@Global() // 全局模块,无需重复导入
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

然后在任何 Service 中直接注入:

// posts/posts.service.ts
@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  async findAll(query: PostQueryDto) {
    const { page, limit } = query;
    const skip = (page - 1) * limit;

    const [total, data] = await Promise.all([
      this.prisma.post.count(),
      this.prisma.post.findMany({
        skip,
        take: limit,
        include: {
          user: { select: { name: true } },
          tags: { include: { tag: true } }
        }
      })
    ]);

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

启动时自动连接,退出时自动断开——这才是企业级应用该有的样子。


🔐 四、JWT 登录:无状态认证,安全又轻量

HTTP 是无状态的,所以我们用 JWT:

// auth/auth.service.ts
import * as bcrypt from 'bcrypt';
import * as jwt from 'jsonwebtoken';

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

  async login(username: string, password: string) {
    const user = await this.prisma.user.findUnique({ where: { name: username } });
    
    if (!user || !bcrypt.compareSync(password, user.password)) {
      throw new UnauthorizedException('用户名或密码错误');
    }

    const payload = { sub: user.id, username: user.name };
    return {
      access_token: jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' })
    };
  }
}

前端每次请求,Axios 拦截器自动注入 token:

// 前端 axios.interceptors.request.use(...)
// headers.Authorization = `Bearer ${token}`

NestJS 中间件验证(简化版):

// auth/auth.middleware.ts
@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) return res.status(401).json({ message: '未登录' });

    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET);
      (req as any).user = payload; // 挂载到 request
      next();
    } catch {
      return res.status(401).json({ message: 'Token 无效' });
    }
  }
}

身份信息直接嵌入 token,免查库(适合非敏感场景)。
如需更高安全,可搭配 Redis 做 token 黑名单(登出时加入黑名单)。


🏗️ 五、工程化思维:从 seeds 到日志,打造完整闭环

1. Seeds:初始化测试数据

// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';

const prisma = new PrismaClient();

async function main() {
  await prisma.user.create({
    data: {
      name: 'admin',
      password: bcrypt.hashSync('123456', 10)
    }
  });
}

main()
  .catch(e => console.error(e))
  .finally(() => prisma.$disconnect());

运行 npx prisma db seed,一键填充初始数据。

2. 环境隔离

.env 文件管理配置:

# .env
DATABASE_URL="postgresql://postgres:123456@localhost:5432/notes_ai"
JWT_SECRET="your-super-secret-jwt-key"

配合 config 模块,不同环境(dev/staging/prod)自动切换。

3. 日志与监控

  • 开发环境:Prisma 打印 SQL(log: ['query']
  • 生产环境:集成 Winston 日志 + Sentry 错误追踪
  • 健康检查:用 @nestjs/terminus 提供 /health 接口

🎯 结语:后端,也可以很“前端”

谁说后端必须枯燥?
用 TypeScript 写数据库模型,用装饰器定义 API,用管道校验参数——每一行代码,都是对混乱的反抗

NestJS + Prisma,不只是工具链,更是一种工程美学
它让后端开发者,也能享受“类型安全”、“模块化”、“可测试”的现代开发乐趣。

💡 最后暴言:当你用 Prisma 写出 post.user.avatars.filename 这种链式查询时,SQL 老炮儿可能会流泪——但你的产品经理会笑。

因为——你交付的不是接口,是确定性