NestJS中用Mongoose实现MongoDB自增字段遇预存钩子问题
解决NestJS中Mongoose自增字段的问题
问题1:pre-save钩子未执行
pre-save钩子不触发的常见原因及解决办法:
- 钩子注册时机错误:必须在Schema编译为Model之前绑定pre-save钩子。在NestJS中,要确保
pre('save')钩子是在@Schema()装饰的类定义之后、MongooseModule.forFeature()注册Schema之前完成绑定的。 - 使用了不触发save的方法:如果代码里用
insertMany()批量创建文档,该方法不会触发pre-save钩子,需换成create()或逐个调用save方法。 - 异步处理异常未捕获:钩子是async函数,要包裹try/catch处理异常,否则异常会导致钩子静默失败。
问题2:Model实例创建方式错误
你代码里new Model<UserDocument>()的写法完全错误,Mongoose的Model不能通过new直接实例化获取。在pre-save钩子的上下文里,this指向当前文档实例,通过this.constructor即可拿到对应的Model:
UserSchema.pre<User>('save', async function (next) { // 通过当前文档的constructor获取对应Model const UserModel = this.constructor as Model<UserDocument>; const lastUser = await UserModel.findOne({}).sort({ number: -1 }).exec(); this.number = lastUser ? lastUser.number + 1 : 1; next(); });
更可靠的自增实现(避免并发冲突)
上面的写法在高并发场景下会出现重复自增数的问题——多个请求可能同时查到相同的lastUser,导致生成重复的number。推荐用单独的计数器集合实现原子操作:
- 定义计数器Schema和Model:
// counter.schema.ts import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; export type CounterDocument = Counter & Document; @Schema() export class Counter { @Prop({ required: true, unique: true }) modelName: string; @Prop({ default: 0 }) seq: number; } export const CounterSchema = SchemaFactory.createForClass(Counter);
- 在User的pre-save钩子中调用计数器:
UserSchema.pre<User>('save', async function (next) { if (this.isNew) { // 仅在创建新文档时触发自增 const CounterModel = await import('./counter.model').then(m => m.CounterModel); const counter = await CounterModel.findOneAndUpdate( { modelName: 'User' }, { $inc: { seq: 1 } }, { new: true, upsert: true } // 不存在则创建,返回更新后的文档 ); this.number = counter.seq; } next(); });
注意:要在NestJS的Module中注册CounterModel:
// 对应业务模块 @Module({ imports: [ MongooseModule.forFeature([ { name: Counter.name, schema: CounterSchema }, { name: User.name, schema: UserSchema } ]) ] }) export class YourModule {}
内容的提问来源于stack exchange,提问作者Faabass
相关产品推荐
相关产品推荐

