NestJS/Mongoose创建子文档前如何校验关联父文档是否存在
NestJS + Mongoose 关联父文档存在性校验实现方案
Mongoose 默认确实不会校验ref关联的文档是否真实存在,你可以根据需求选择以下两种实现方式,生产环境推荐组合使用做双层校验。
方案1:Mongoose 前置中间件(数据层兜底,最可靠)
直接在Schema层面绑定写入前置钩子,所有调用Book模型写入数据的路径(接口、脚本、定时任务等)都会触发校验,不会出现漏校验的情况,是防脏数据的核心兜底手段。
你只需要在BookSchema定义完成后添加对应钩子即可:
// book.schema.ts 补充以下代码 import { Writer } from 'src/writer/schemas/writer.schema'; // 单条文档保存校验 BookSchema.pre('save', async function(next) { const isWriterExisted = await Writer.model.exists({ _id: this.writer }); if (!isWriterExisted) { next(new Error(`关联作者ID ${this.writer} 不存在,无法保存书籍记录`)); return; } next(); }); // 批量插入校验(如果用到insertMany方法就加) BookSchema.pre('insertMany', async function(next, docs) { // 去重提取所有待校验的作者ID const writerIds = [...new Set(docs.map(doc => doc.writer))]; const existedWriters = await Writer.model.find({ _id: { $in: writerIds } }).select('_id'); const existedIdSet = new Set(existedWriters.map(w => w._id.toString())); const invalidIds = writerIds.filter(id => !existedIdSet.has(id.toString())); if (invalidIds.length) { next(new Error(`以下关联作者ID不存在:${invalidIds.join(',')},无法批量插入书籍记录`)); return; } next(); });
- 优点:和业务逻辑完全解耦,覆盖所有数据写入场景
- 缺点:校验逻辑在数据库操作层触发,无效请求会走到数据库连接环节,接口响应速度略慢
方案2:NestJS 全局管道 + 自定义校验器(接口层前置校验)
如果你的接口用DTO接收参数,可以配合class-validator和NestJS自带的ValidationPipe,在请求进入Controller之前就完成校验,提前返回错误,减少无效数据库请求。
实现步骤:
- 安装必要依赖
npm i class-validator class-transformer
- 编写自定义异步校验器
// src/book/validators/is-writer-exists.validator.ts import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Writer, WriterDocument } from 'src/writer/schemas/writer.schema'; @ValidatorConstraint({ async: true }) @Injectable() export class IsWriterExistsConstraint implements ValidatorConstraintInterface { constructor(@InjectModel(Writer.name) private readonly writerModel: Model<WriterDocument>) {} async validate(writerId: string): Promise<boolean> { return !!await this.writerModel.exists({ _id: writerId }); } defaultMessage(): string { return '传入的关联作者ID不存在,请检查后重试'; } } export function IsWriterExists(validationOptions?: ValidationOptions) { return function (target: Object, propertyName: string) { registerDecorator({ target: target.constructor, propertyName, options: validationOptions, constraints: [], validator: IsWriterExistsConstraint, }); }; }
- 在Book模块中注册校验器
// src/book/book.module.ts import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Book, BookSchema } from './schemas/book.schema'; import { Writer, WriterSchema } from 'src/writer/schemas/writer.schema'; import { IsWriterExistsConstraint } from './validators/is-writer-exists.validator'; // 其他你自己的controller、service导入保持不变 @Module({ imports: [ MongooseModule.forFeature([ { name: Book.name, schema: BookSchema }, { name: Writer.name, schema: WriterSchema } ]) ], providers: [IsWriterExistsConstraint /* 你原来的service照常写 */], // controllers配置不变 }) export class BookModule {}
- 在创建Book的DTO中使用校验装饰器
// src/book/dto/create-book.dto.ts import { IsMongoId, IsNotEmpty, IsString } from 'class-validator'; import { IsWriterExists } from '../validators/is-writer-exists.validator'; export class CreateBookDto { @IsMongoId({ message: '作者ID格式不合法' }) @IsNotEmpty() @IsWriterExists() writer: string; @IsString() @IsNotEmpty({ message: '书籍名称不能为空' }) name: string; }
- 优点:校验在接口最外层触发,响应速度快,和参数格式校验逻辑整合,错误返回更统一
- 缺点:只能拦截走对应DTO的接口请求,脚本、其他服务直接调用Model写入时不会触发,无法单独作为唯一校验手段
方案选型说明
- 不推荐用Guard做这类校验:Guard的设计定位是权限控制(比如判断登录状态、用户是否有操作权限),不适合做数据合法性校验
- 生产环境最佳实践:两种方案搭配使用,接口层用管道提前拦截无效请求,Schema层用前置中间件做兜底,完全避免脏数据产生
内容的提问来源于stack exchange,提问作者ACl
相关产品推荐
相关产品推荐

