1046 字
5 分钟
NestJS 学习记录 Part 7:DTO 嵌套验证、全局 404 与大数据批量插入

本文记录在 NestJS 进阶开发中遇到的三个具体场景:复杂的 DTO 嵌套验证陷阱、基于抛出异常的优雅 404 处理,以及针对海量数据的 TypeORM 批量插入策略。


1. 为什么 DTO 的嵌套验证必须加 @Type 装饰器?#

在处理用户注册时,前端往往会传过来一个多层嵌套的复杂 JSON 结构(例如包含 profileaddressInfo)。

在 DTO 中,我们通常会写下这样的代码:

apps/server/src/user/dto/create-user.dto.ts
export class CreateUserDto {
// ...
@IsOptional()
@ValidateNested()
@Type(() => ProfileDto) // ⚠️ 这一行绝对不能漏掉!
profile?: ProfileDto;
}

思考与对比#

  • 如果只写 @ValidateNested() 会怎样? 如果漏写了 @Type,即使你在 ProfileDto 内部写满了 @IsString()@IsNotEmpty(),这些针对 profile 内部字段的校验也完全不会生效,脏数据会长驱直入保存到数据库中。这是一个极度危险的安全隐患。
  • 为什么要配合 @Type 网络传输过来的 JSON 只是一个纯粹的、无原型的 Objectclass-validator 必须要在类的实例 (Instance) 上才能读取到那些装饰器元数据并执行校验。 @Type(() => ProfileDto)class-transformer 提供的方法,它的作用是在校验开始前,先把普通的 JSON Object 真正实例化为 ProfileDto 类的对象。有了实例,@ValidateNested() 才能顺藤摸瓜,触发内部属性的校验规则。

2. 优雅处理 404:findOneOrFail 与全局异常拦截#

在业务代码中,我们最常写的逻辑就是“先查询,如果不存在就报错”:

// 传统写法:代码臃肿
const user = await this.userRepository.findOneBy({ id });
if (!user) {
throw new NotFoundException("请求的资源不存在");
}
return this.userRepository.remove(user);

而在我们的项目中,删除逻辑被简化为了纯粹的 Happy Path:

apps/server/src/user/user.service.ts
async remove(id: number) {
const user = await this.userRepository.findOneByOrFail({ id });
return this.userRepository.remove(user);
}

配套的,我们增加了一个专门针对 TypeORM 的异常过滤器:

apps/server/src/filters/entity-not-found-exception.filter.ts
@Catch(EntityNotFoundError)
export class EntityNotFoundExceptionFilter implements ExceptionFilter {
// ...
catch(exception: EntityNotFoundError, host: ArgumentsHost) {
// 捕获 TypeORM 的 EntityNotFoundError,转换为 HTTP 404 给前端
sendFormattedExceptionResponse(response, request, this.logger, {
statusCode: HttpStatus.NOT_FOUND,
message: "请求的资源不存在",
exceptionName: exception.name,
errorMessage: exception.message,
});
}
}

思考与对比#

  • 为什么不手动 if (!user) throw Error 在复杂的业务线中,各种查询散落在不同的 Service 里。每次都要手写这三行判断代码,极大地增加了代码的噪音。
  • OrFail 机制的优势: TypeORM 提供了 findOneOrFailfindOneByOrFail,当查询不到数据时,它会在底层直接抛出 EntityNotFoundError。 我们利用全局过滤器在框架顶层捕获这个异常,并统一转换为 HTTP 404。这使得我们的 Service 层代码异常干净,完全不需要关注异常的分发,只需关注正确的业务流。

3. 防止内存溢出:TypeORM 海量数据批量插入#

在做初始化脚本(如导入全国省市区行政区划数据)时,我们需要将解析出的海量数据一次性写入数据库。

apps/server/src/scripts/seed-region.ts
console.log(
`总共提取出 ${regionsToInsert.length} 条行政区划数据,准备执行插入...`,
);
// 经典的大数据分块插入 (Chunking)
const chunkSize = 1000;
for (let i = 0; i < regionsToInsert.length; i += chunkSize) {
const chunk = regionsToInsert.slice(i, i + chunkSize);
await regionRepo.save(chunk);
console.log(
`已插入 ${Math.min(i + chunkSize, regionsToInsert.length)} / ${regionsToInsert.length} 条...`,
);
}

思考与对比#

  • 如果直接 await regionRepo.save(regionsToInsert) 会发生什么? 这往往会导致两个灾难性的后果:
    1. Node.js OOM (内存溢出):TypeORM 在执行 save 时,会在内存中为每一个实体对象生成极其复杂的依赖图和查询构建树。几万条数据瞬间就会把 V8 引擎的堆内存撑爆。
    2. 数据库拒收:哪怕 Node.js 撑住了,它拼接出来的那条超级巨大的 INSERT INTO SQL 语句,极有可能超出数据库配置的包大小限制(例如 MySQL 的 max_allowed_packet,默认通常只有几 MB),导致写入直接失败。
  • Chunking (分块) 的工程化思维: 通过切割数组,每次只处理 1000 条数据。这不仅将内存占用控制在了一个极低的安全水位,还巧妙避开了数据库的包体积限制,是后端处理批量导入的标准化最佳实践。
NestJS 学习记录 Part 7:DTO 嵌套验证、全局 404 与大数据批量插入
https://nollieleo.github.io/posts/nestjs学习记录-part7/
作者
翁先森
发布于
2026-03-26
许可协议
CC BY-NC-SA 4.0