1046 字
5 分钟
NestJS 学习记录 Part 7:DTO 嵌套验证、全局 404 与大数据批量插入
本文记录在 NestJS 进阶开发中遇到的三个具体场景:复杂的 DTO 嵌套验证陷阱、基于抛出异常的优雅 404 处理,以及针对海量数据的 TypeORM 批量插入策略。
1. 为什么 DTO 的嵌套验证必须加 @Type 装饰器?
在处理用户注册时,前端往往会传过来一个多层嵌套的复杂 JSON 结构(例如包含 profile 和 addressInfo)。
在 DTO 中,我们通常会写下这样的代码:
export class CreateUserDto { // ... @IsOptional() @ValidateNested() @Type(() => ProfileDto) // ⚠️ 这一行绝对不能漏掉! profile?: ProfileDto;}思考与对比
- 如果只写
@ValidateNested()会怎样? 如果漏写了@Type,即使你在ProfileDto内部写满了@IsString()、@IsNotEmpty(),这些针对profile内部字段的校验也完全不会生效,脏数据会长驱直入保存到数据库中。这是一个极度危险的安全隐患。 - 为什么要配合
@Type? 网络传输过来的 JSON 只是一个纯粹的、无原型的Object。class-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:
async remove(id: number) { const user = await this.userRepository.findOneByOrFail({ id }); return this.userRepository.remove(user);}配套的,我们增加了一个专门针对 TypeORM 的异常过滤器:
@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 提供了
findOneOrFail或findOneByOrFail,当查询不到数据时,它会在底层直接抛出EntityNotFoundError。 我们利用全局过滤器在框架顶层捕获这个异常,并统一转换为 HTTP 404。这使得我们的 Service 层代码异常干净,完全不需要关注异常的分发,只需关注正确的业务流。
3. 防止内存溢出:TypeORM 海量数据批量插入
在做初始化脚本(如导入全国省市区行政区划数据)时,我们需要将解析出的海量数据一次性写入数据库。
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)会发生什么? 这往往会导致两个灾难性的后果:- Node.js OOM (内存溢出):TypeORM 在执行
save时,会在内存中为每一个实体对象生成极其复杂的依赖图和查询构建树。几万条数据瞬间就会把 V8 引擎的堆内存撑爆。 - 数据库拒收:哪怕 Node.js 撑住了,它拼接出来的那条超级巨大的
INSERT INTOSQL 语句,极有可能超出数据库配置的包大小限制(例如 MySQL 的max_allowed_packet,默认通常只有几 MB),导致写入直接失败。
- Node.js OOM (内存溢出):TypeORM 在执行
- Chunking (分块) 的工程化思维: 通过切割数组,每次只处理 1000 条数据。这不仅将内存占用控制在了一个极低的安全水位,还巧妙避开了数据库的包体积限制,是后端处理批量导入的标准化最佳实践。
NestJS 学习记录 Part 7:DTO 嵌套验证、全局 404 与大数据批量插入
https://nollieleo.github.io/posts/nestjs学习记录-part7/