{T}

NestJS从Controller到数据库完整链路实现

NestJS从Controller到数据库完整链路实现

学习目标:打通从 Controller 到数据库的完整链路,掌握分页查询、参数转换、路径别名、全局模块、Prisma 事务处理。


一、完整链路实现流程

1.1 从 Controller 到数据库的完整流程

code
完整链路流程:
│
├── 第一步:前端发送请求
│   └── GET /home?page=1&size=10
│
├── 第二步:Controller 接收请求
│   ├── 获取 Query 参数(page、size)
│   ├── 使用 Pipe 转换参数类型(string → number)
│   └── 调用 Service 方法
│
├── 第三步:Service 处理业务逻辑
│   ├── 接收 Controller 传递的参数
│   ├── 调用 Prisma 查询数据库
│   └── 返回数据给 Controller
│
├── 第四步:Prisma 操作数据库
│   ├── 使用 findMany() 查询数据
│   ├── 使用 count() 统计总数
│   └── 使用 $transaction() 处理事务
│
├── 第五步:数据库返回数据
│   └── PostgreSQL 返回查询结果
│
├── 第六步:Prisma 映射数据
│   └── 将 SQL 结果映射为对象
│
├── 第七步:Service 返回数据
│   └── 将 [data, total] 返回给 Controller
│
├── 第八步:Controller 封装响应
│   ├── 解构数组为对象
│   └── 返回 { data, total }
│
└── 第九步:前端接收响应
    └── { data: [...], total: number }

1.2 各层职责回顾

层级职责关键操作
Controller接收请求、参数解析、响应封装获取参数、调用 Service、封装响应
Service业务逻辑、数据转换分页计算、调用 Prisma、事务处理
PrismaORM 操作、SQL 生成findMany、count、$transaction
Database数据持久化存储、查询

二、分页查询实现

2.1 获取分页参数

Controller 层

typescript
// src/modules/home/home.controller.ts
@Controller('home')
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get()
  async findAll(
    @Query('page', ParseIntPipe) page: number = 1,
    @Query('size', ParseIntPipe) size: number = 10,
  ) {
    const [data, total] = await this.homeService.findAll(page, size);
    return { data, total };
  }
}

参数说明

  • page:页码,默认为 1
  • size:每页条数,默认为 10
  • ParseIntPipe:将 string 转换为 number

请求示例

code
GET /home?page=1&size=10
GET /home?page=2&size=20

2.2 Query 参数的类型问题

问题:在 NestJS 中,通过 @Query() 获取的参数类型永远是 string

测试代码

typescript
@Get()
async findAll(
  @Query('page') page: number,  // TypeScript 类型是 number
  @Query('size') size: number,  // 但实际值是 string
) {
  console.log(typeof page);  // 输出:'string'
  console.log(typeof size);  // 输出:'string'
}

解决方案:使用 Pipe 进行类型转换。


三、NestJS Pipes(管道)详解

3.1 Pipes 的作用

code
Pipes(管道)的两大作用:
│
├── 1. 数据转换(Transformation)
│   ├── 将输入数据转换为所需格式
│   ├── 例如:string → number
│   └── 例如:日期字符串 → Date 对象
│
└── 2. 数据校验(Validation)
    ├── 验证输入数据是否符合要求
    ├── 例如:是否为空、是否为有效邮箱
    └── 校验失败抛出异常

3.2 内置 Pipe 详解

NestJS 提供的内置 Pipe

Pipe作用示例
ParseIntPipe转换为整数@Query('page', ParseIntPipe) page: number
ParseFloatPipe转换为浮点数@Query('price', ParseFloatPipe) price: number
ParseBoolPipe转换为布尔值@Query('active', ParseBoolPipe) active: boolean
ParseArrayPipe转换为数组@Query('ids', ParseArrayPipe) ids: number[]
ParseUUIDPipe验证 UUID@Param('id', ParseUUIDPipe) id: string
ParseEnumPipe验证枚举值@Query('status', new ParseEnumPipe(Status))
DefaultValuePipe设置默认值@Query('page', new DefaultValuePipe(1))

3.3 ParseIntPipe 使用示例

基本用法

typescript
import { Controller, Get, Query, ParseIntPipe } from '@nestjs/common';

@Controller('home')
export class HomeController {
  @Get()
  async findAll(
    @Query('page', ParseIntPipe) page: number = 1,
    @Query('size', ParseIntPipe) size: number = 10,
  ) {
    // page 和 size 现在都是 number 类型
    return this.homeService.findAll(page, size);
  }
}

自定义错误消息

typescript
@Get()
async findAll(
  @Query('page', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  page: number,
) {
  return this.homeService.findAll(page);
}

3.4 DefaultValuePipe 使用示例

设置默认值

typescript
import { Controller, Get, Query, DefaultValuePipe } from '@nestjs/common';

@Controller('home')
export class HomeController {
  @Get()
  async findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,
  ) {
    return this.homeService.findAll(page, size);
  }
}

四、路径别名配置

4.1 配置路径别名

修改 tsconfig.json

json
{
  "compilerOptions": {
    "baseUrl": "./",  // 必须设置 baseUrl
    "paths": {
      "src/*": ["src/*"],
      "@/*": ["src/*"]
    }
  }
}

配置说明

  • baseUrl:设置根目录,必须配置
  • paths:配置路径别名映射
  • src/*:映射 src 目录下的所有文件
  • @/*:使用 @ 符号映射 src 目录

4.2 使用路径别名

使用前

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

使用后

typescript
// 方式一:使用 src 别名
import { PrismaService } from 'src/prisma/prisma.service';

// 方式二:使用 @ 别名(推荐)
import { PrismaService } from '@/prisma/prisma.service';

注意事项

  • baseUrl 必须设置,否则 paths 不生效
  • 路径别名只在 TypeScript 编译时生效
  • 运行时仍需要相对路径或绝对路径

五、全局模块设置

5.1 为什么需要全局模块?

问题场景

  • PrismaModule 需要在所有功能模块中使用
  • 每个模块都要导入 PrismaModule,非常繁琐
  • 容易遗漏导入,导致依赖注入失败

传统方式(繁琐)

typescript
// 每个模块都要导入 PrismaModule
@Module({
  imports: [PrismaModule],
  providers: [HomeService],
  controllers: [HomeController],
})
export class HomeModule {}

5.2 使用 @Global 装饰器

设置全局模块

typescript
// 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 {}

使用全局模块

typescript
// 其他模块无需导入 PrismaModule
@Module({
  providers: [HomeService],
  controllers: [HomeController],
})
export class HomeModule {}

Service 中直接使用

typescript
// src/modules/home/home.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';

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

  // 无需导入 PrismaModule,直接使用 PrismaService
  async findAll(page: number, size: number) {
    return this.prisma.homeResources.findMany();
  }
}

5.3 全局模块注意事项

code
全局模块使用规则:
│
├── 1. 使用 @Global() 装饰器
│   └── 在 @Module() 装饰器之前
│
├── 2. 必须导出 providers
│   └── exports: [PrismaService]
│
├── 3. 无需重复导入
│   └── 其他模块无需 imports: [PrismaModule]
│
├── 4. 全局模块只需注册一次
│   └── 通常在 AppModule 中导入
│
└── 5. 谨慎使用
    ├── 只对真正全局的服务使用
    └── 例如:PrismaService、ConfigService、LoggerService

六、Prisma 分页查询详解

6.1 Prisma 分页参数

findMany() 分页参数

typescript
interface FindManyOptions {
  skip?: number;  // 跳过多少条
  take?: number;  // 获取多少条
}

分页计算公式

typescript
// 前端传递:page(页码)、size(每页条数)
// Prisma 需要:skip(跳过条数)、take(获取条数)

const skip = (page - 1) * size;  // 跳过的条数
const take = size;               // 获取的条数

// 示例:
// page=1, size=10 → skip=0, take=10
// page=2, size=10 → skip=10, take=10
// page=3, size=10 → skip=20, take=10

6.2 完整的分页查询实现

Service 层

typescript
// src/modules/home/home.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';

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

  async findAll(page: number = 1, size: number = 10) {
    // 计算分页参数
    const skip = (page - 1) * size;
    const take = size;

    // 查询数据和总数
    const [data, total] = await this.prisma.$transaction([
      this.prisma.homeResources.findMany({
        skip,
        take,
        orderBy: { order: 'asc' },
      }),
      this.prisma.homeResources.count(),
    ]);

    return [data, total];
  }
}

Controller 层

typescript
// src/modules/home/home.controller.ts
import { Controller, Get, Query, ParseIntPipe } from '@nestjs/common';
import { HomeService } from './home.service';

@Controller('home')
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get()
  async findAll(
    @Query('page', ParseIntPipe) page: number = 1,
    @Query('size', ParseIntPipe) size: number = 10,
  ) {
    const [data, total] = await this.homeService.findAll(page, size);
    return { data, total };
  }
}

响应格式

json
{
  "data": [
    {
      "id": 1,
      "title": "资源1",
      "order": 100
    },
    {
      "id": 2,
      "title": "资源2",
      "order": 200
    }
  ],
  "total": 100
}

七、Prisma 事务处理

7.1 事务的作用

code
事务(Transaction)的作用:
│
├── 1. 原子性(Atomicity)
│   ├── 一系列操作要么全部成功,要么全部失败
│   └── 部分失败会回滚已成功的操作
│
├── 2. 一致性(Consistency)
│   ├── 数据库状态保持一致
│   └── 事务前后数据完整性不变
│
├── 3. 隔离性(Isolation)
│   ├── 多个事务互不干扰
│   └── 并发执行结果与串行执行一致
│
└── 4. 持久性(Durability)
    ├── 事务完成后数据永久保存
    └── 即使系统故障也不丢失

7.2 Prisma $transaction 使用

基本语法

typescript
const [result1, result2, ...] = await prisma.$transaction([
  prisma.model1.operation1(),
  prisma.model2.operation2(),
  ...
]);

使用场景一:查询数据 + 统计总数

typescript
// 同时查询数据和总数
const [data, total] = await this.prisma.$transaction([
  this.prisma.homeResources.findMany({
    skip: 0,
    take: 10,
  }),
  this.prisma.homeResources.count(),
]);

// data: Array<HomeResources>
// total: number

使用场景二:创建订单 + 扣减库存

typescript
const [order, inventory] = await this.prisma.$transaction([
  // 创建订单
  this.prisma.order.create({
    data: { userId: 1, productId: 1, quantity: 2 },
  }),
  // 扣减库存
  this.prisma.inventory.update({
    where: { productId: 1 },
    data: { stock: { decrement: 2 } },
  }),
]);

// 如果库存不足,订单创建也会回滚

使用场景三:批量操作

typescript
// 批量创建用户
const result = await this.prisma.$transaction(
  users.map(user => 
    this.prisma.user.create({ data: user })
  )
);

7.3 $transaction 注意事项

typescript
//  正确:数组形式(并行执行)
const [data, total] = await this.prisma.$transaction([
  this.prisma.user.findMany(),
  this.prisma.user.count(),
]);

//  正确:回调形式(顺序执行,可回滚)
const result = await this.prisma.$transaction(async (tx) => {
  // 创建用户
  const user = await tx.user.create({ data: { name: 'Tom' } });
  
  // 创建用户配置
  const config = await tx.userConfig.create({
    data: { userId: user.id, theme: 'dark' },
  });
  
  return { user, config };
});

//  错误:事务中不能使用非事务的 prisma 实例
await this.prisma.$transaction(async (tx) => {
  await tx.user.create({ data: { name: 'Tom' } });
  await this.prisma.userConfig.create({ data: { theme: 'dark' } });  // 错误!
});

八、Controller 与 Service 分层设计

8.1 为什么需要分层?

code
分层设计的优势:
│
├── 1. 职责分离
│   ├── Controller:处理 HTTP 请求、响应
│   ├── Service:处理业务逻辑
│   └── Repository:处理数据访问
│
├── 2. 可读性强
│   ├── 代码结构清晰
│   ├── 职责明确
│   └── 易于理解
│
├── 3. 复用性强
│   ├── Service 可在多个 Controller 中使用
│   ├── 避免重复代码
│   └── 易于测试
│
└── 4. 易于维护
    ├── 修改业务逻辑只改 Service
    ├── 修改接口只改 Controller
    └── 互不影响

8.2 Controller 层职责

typescript
@Controller('home')
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get()
  async findAll(
    @Query('page', ParseIntPipe) page: number = 1,
    @Query('size', ParseIntPipe) size: number = 10,
  ) {
    //  Controller 层职责:
    // 1. 获取请求参数
    // 2. 调用 Service 方法
    // 3. 封装响应数据
    // 4. 返回给前端

    const [data, total] = await this.homeService.findAll(page, size);
    return { data, total };  // 封装响应格式
  }
}

Controller 层职责清单

职责说明示例
获取参数使用装饰器获取请求参数@Query(), @Body(), @Param()
参数转换使用 Pipe 转换参数类型ParseIntPipe, ParseBoolPipe
参数校验使用 DTO 校验参数class-validator
调用 Service将参数传递给 Servicethis.homeService.findAll(page, size)
封装响应将数据封装为统一格式{ data, total }
处理异常捕获并处理异常try-catch

8.3 Service 层职责

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

  async findAll(page: number = 1, size: number = 10) {
    //  Service 层职责:
    // 1. 业务逻辑处理(分页计算)
    // 2. 调用数据库(Prisma)
    // 3. 数据处理(事务)
    // 4. 返回原始数据给 Controller

    const skip = (page - 1) * size;
    const take = size;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.homeResources.findMany({
        skip,
        take,
        orderBy: { order: 'asc' },
      }),
      this.prisma.homeResources.count(),
    ]);

    return [data, total];  // 返回原始数据
  }
}

Service 层职责清单

职责说明示例
业务逻辑实现具体业务逻辑分页计算、数据过滤
数据访问调用数据库操作prisma.findMany()
事务处理管理数据库事务$transaction()
数据转换数据格式转换DTO → Entity
缓存处理处理缓存逻辑Redis 缓存
异常处理抛出业务异常NotFoundException

8.4 为什么要分离 Controller 和 Service?

** 不推荐:将逻辑写在 Controller 中**:

typescript
@Get()
async findAll(@Query('page') page: string, @Query('size') size: string) {
  //  不推荐:Controller 中写业务逻辑
  const pageNum = parseInt(page);
  const sizeNum = parseInt(size);
  const skip = (pageNum - 1) * sizeNum;
  const take = sizeNum;

  const [data, total] = await this.prisma.$transaction([
    this.prisma.homeResources.findMany({ skip, take }),
    this.prisma.homeResources.count(),
  ]);

  return { data, total };
}

** 推荐:Controller 和 Service 分离**:

typescript
// Controller:只负责接收请求和响应
@Get()
async findAll(
  @Query('page', ParseIntPipe) page: number = 1,
  @Query('size', ParseIntPipe) size: number = 10,
) {
  const [data, total] = await this.homeService.findAll(page, size);
  return { data, total };
}

// Service:负责业务逻辑
async findAll(page: number = 1, size: number = 10) {
  const skip = (page - 1) * size;
  const take = size;

  const [data, total] = await this.prisma.$transaction([
    this.prisma.homeResources.findMany({ skip, take }),
    this.prisma.homeResources.count(),
  ]);

  return [data, total];
}

分层的好处

code
分层设计的好处:
│
├── 1. 可读性
│   ├── Controller:清晰看到所有接口
│   ├── Service:清晰看到所有业务逻辑
│   └── 易于理解和维护
│
├── 2. 复用性
│   ├── HomeService 可在其他模块使用
│   ├── 例如:在 OrderService 中使用 HomeService
│   └── 避免重复代码
│
├── 3. 可测试性
│   ├── 单独测试 Service 层
│   ├── 单独测试 Controller 层
│   └── 易于编写单元测试
│
└── 4. 可维护性
    ├── 修改业务逻辑只改 Service
    ├── 修改接口格式只改 Controller
    └── 互不影响

九、DI 系统工作原理回顾

9.1 DI 系统核心概念

code
依赖注入(DI)工作原理:
│
├── 第一步:在 constructor 中声明依赖
│   └── constructor(private homeService: HomeService) {}
│
├── 第二步:DI 系统查找依赖
│   ├── 检查当前模块的 providers
│   ├── 检查导入模块的 exports
│   └── 检查全局模块的 exports
│
├── 第三步:DI 系统创建实例
│   ├── 单例模式:只创建一次
│   ├── 自动注入依赖
│   └── 保存到 DI 容器中
│
└── 第四步:使用实例
    └── this.homeService.findAll()

9.2 constructor 的作用

constructor 中声明依赖

typescript
@Injectable()
export class HomeService {
  // 在 constructor 中声明依赖
  constructor(private prisma: PrismaService) {}
  
  // 等价于:
  // this.prisma = new PrismaService();
  // 但是由 DI 系统自动完成
}

等价的传统写法

typescript
//  传统写法:手动创建实例
export class HomeService {
  private prisma: PrismaService;
  
  constructor() {
    this.prisma = new PrismaService();
  }
}

//  DI 写法:自动注入实例
@Injectable()
export class HomeService {
  constructor(private prisma: PrismaService) {}
}

DI 的优势

code
DI 系统的优势:
│
├── 1. 解耦
│   └── 不需要手动创建实例
│
├── 2. 单例
│   └── 全局共享一个实例
│
├── 3. 自动注入
│   └── 自动解析和注入依赖
│
└── 4. 易于测试
    └── 可以轻松替换依赖进行测试

9.3 跨模块使用 Service

场景:在 HomeModule 中使用 PrismaService

方式一:导入 PrismaModule(传统方式)

typescript
// home.module.ts
@Module({
  imports: [PrismaModule],  // 导入 PrismaModule
  providers: [HomeService],
  controllers: [HomeController],
})
export class HomeModule {}

方式二:使用全局模块(推荐)

typescript
// prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// home.module.ts
@Module({
  // 无需导入 PrismaModule
  providers: [HomeService],
  controllers: [HomeController],
})
export class HomeModule {}

十、完整实战示例

10.1 项目结构

code
src/
├── prisma/
│   ├── prisma.module.ts      # 全局模块
│   └── prisma.service.ts     # Prisma 服务
├── modules/
│   └── home/
│       ├── home.module.ts
│       ├── home.controller.ts
│       ├── home.service.ts
│       └── dto/
│           ├── create-home-resource.dto.ts
│           └── update-home-resource.dto.ts
├── app.module.ts
└── main.ts

10.2 完整代码清单

1. PrismaModule(全局模块)

typescript
// 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 {}

2. PrismaService

typescript
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

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

3. HomeModule

typescript
// src/modules/home/home.module.ts
import { Module } from '@nestjs/common';
import { HomeController } from './home.controller';
import { HomeService } from './home.service';

@Module({
  controllers: [HomeController],
  providers: [HomeService],
})
export class HomeModule {}

4. HomeController

typescript
// src/modules/home/home.controller.ts
import { Controller, Get, Query, ParseIntPipe } from '@nestjs/common';
import { HomeService } from './home.service';

@Controller('home')
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get()
  async findAll(
    @Query('page', ParseIntPipe) page: number = 1,
    @Query('size', ParseIntPipe) size: number = 10,
  ) {
    const [data, total] = await this.homeService.findAll(page, size);
    return { data, total };
  }
}

5. HomeService

typescript
// src/modules/home/home.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';

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

  async findAll(page: number = 1, size: number = 10) {
    const skip = (page - 1) * size;
    const take = size;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.homeResources.findMany({
        skip,
        take,
        orderBy: { order: 'asc' },
      }),
      this.prisma.homeResources.count(),
    ]);

    return [data, total];
  }
}

6. AppModule

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { HomeModule } from './modules/home/home.module';

@Module({
  imports: [PrismaModule, HomeModule],
})
export class AppModule {}

10.3 测试示例

请求测试

bash
# 查询第一页,每页 10 条
GET http://localhost:3000/home?page=1&size=10

# 响应
{
  "data": [
    {
      "id": 1,
      "title": "资源1",
      "order": 100
    },
    {
      "id": 2,
      "title": "资源2",
      "order": 200
    }
  ],
  "total": 100
}

分页计算验证

bash
# page=1, size=10 → skip=0, take=10
# 返回第 1-10 条数据

# page=2, size=10 → skip=10, take=10
# 返回第 11-20 条数据

# page=3, size=10 → skip=20, take=10
# 返回第 21-30 条数据

十一、常见问题与解决方案

11.1 Query 参数类型问题

问题@Query() 获取的参数类型是 string,不是 number

解决方案:使用 ParseIntPipe 转换。

typescript
//  错误:直接使用,类型是 string
@Query('page') page: number

//  正确:使用 ParseIntPipe 转换
@Query('page', ParseIntPipe) page: number

//  正确:设置默认值
@Query('page', ParseIntPipe) page: number = 1

11.2 路径别名不生效

问题:配置了 tsconfig.jsonpaths,但路径别名不生效。

解决方案:确保 baseUrl 已设置。

json
{
  "compilerOptions": {
    "baseUrl": "./",  // 必须设置
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

11.3 PrismaService 注入失败

问题PrismaService 依赖注入失败。

解决方案一:导入 PrismaModule

typescript
@Module({
  imports: [PrismaModule],
  providers: [HomeService],
})
export class HomeModule {}

解决方案二:使用全局模块

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

11.4 分页计算错误

问题:分页查询返回的数据不正确。

常见错误

typescript
//  错误:skip 计算错误
const skip = page * size;  // 应该是 (page - 1) * size

//  正确:skip 计算
const skip = (page - 1) * size;

十二、学习要点总结

12.1 核心知识点

code
本节核心知识点:
│
├── 完整链路实现
│   ├── Controller → Service → Prisma → Database
│   ├── 参数传递流程
│   └── 响应数据封装
│
├── 分页查询
│   ├── 获取 page 和 size 参数
│   ├── 使用 ParseIntPipe 转换类型
│   ├── 计算 skip 和 take
│   └── 使用 $transaction 查询数据和总数
│
├── Pipes(管道)
│   ├── 数据转换(string → number)
│   ├── 数据校验(后续学习)
│   └── 内置 Pipe:ParseIntPipe、DefaultValuePipe
│
├── 路径别名
│   ├── tsconfig.json 配置 baseUrl 和 paths
│   └── 使用 @/* 替代相对路径
│
├── 全局模块
│   ├── @Global() 装饰器
│   ├── 无需重复导入
│   └── PrismaModule 设置为全局模块
│
├── Prisma 事务
│   ├── $transaction() 的作用
│   ├── 原子性、一致性
│   └── 查询数据 + 统计总数
│
└── DI 系统
    ├── constructor 中声明依赖
    ├── DI 系统自动注入实例
    └── 单例模式

12.2 学习路径规划

code
学习路径:
│
├── 第一阶段:理解概念(1 天)
│   ├── 理解完整链路流程
│   ├── 理解 Pipes 的作用
│   └── 理解 DI 系统原理
│
├── 第二阶段:实践使用(2-3 天)
│   ├── 实现完整的分页查询
│   ├── 配置路径别名
│   └── 使用全局模块
│
└── 第三阶段:深入应用(持续)
    ├── 自定义 Pipe(下一节)
    ├── 参数校验(下一节)
    └── 复杂事务处理

12.3 重要程度标注

code
重要程度说明:
│
├──  必须掌握
│   ├── 完整链路实现流程
│   ├── 分页查询实现
│   ├── ParseIntPipe 的使用
│   ├── 全局模块的设置
│   └── Prisma $transaction 的使用
│
├──  重要
│   ├── 路径别名配置
│   ├── Controller 和 Service 分层设计
│   ├── DI 系统工作原理
│   └── 分页计算公式
│
└──  了解
    ├── Pipe 的其他类型
    ├── 事务的高级用法
    └── $transaction 的回调形式

重要提示:这一节是 NestJS 从 Controller 到数据库完整链路的核心内容,理解分页查询、参数转换、全局模块、Prisma 事务,对实际项目开发非常重要!特别是要掌握 ParseIntPipe$transaction() 的使用,这在实际项目中非常实用!

下一节预告:下一节将学习自定义 Pipe 和参数校验,包括 class-validatorclass-transformer 的使用。