如何在Mongoose的Save中间件中实现自增ID/计数器?
如何在Mongoose中间件中实现自增计数器(仅新增文档时生效)
核心问题拆解
- Mongoose自动生成的
_id字段不可覆盖,必须用自定义字段(比如pageIndex)存储自增索引 - 直接查询集合总长度的方式有并发风险:多个请求同时新增时会拿到相同数值,导致计数重复
- 你的Schema里没声明计数器字段,所以中间件里赋值不会生效
解决方案:独立计数器集合+原子操作
用单独的集合存储当前自增数值,借助MongoDB的原子操作findOneAndUpdate确保计数唯一递增,彻底解决并发冲突问题。
步骤1:定义计数器模型
先创建一个专门存计数器的集合,用来记录Page文档的最新索引值:
import mongoose from 'mongoose'; const counterSchema = new mongoose.Schema({ _id: { type: String, required: true }, // 计数器唯一标识 seq: { type: Number, default: 0 } // 当前计数 }); const Counter = mongoose.model('Counter', counterSchema);
步骤2:修改Page模型与中间件
- 在Page的Schema里添加自定义自增字段
pageIndex - 仅在新增文档时(通过
this.isNew判断)触发计数逻辑 - 用原子操作获取并递增计数器值,赋值给新文档
完整修正代码
import mongoose from 'mongoose' import { updateIfCurrentPlugin } from 'mongoose-update-if-current' // 修正TypeScript接口:继承mongoose.Document而非mongoose.Page interface PageDoc extends mongoose.Document { pageIndex: number // 自定义自增索引字段 name: string } interface PageAttrs { name: string } interface PageModel extends mongoose.Model<PageDoc> { build(attrs: PageAttrs): PageDoc } const PageSchema = new mongoose.Schema<PageDoc>( { name: { type: String, required: true }, pageIndex: { type: Number, unique: true, // 强制唯一,避免重复 required: true } }, { toJSON: { transform(doc, ret) { ret.id = ret._id delete ret._id delete ret.__v } } } ) // 仅在新增文档时执行计数逻辑 PageSchema.pre('save', async function (next) { if (!this.isNew) return next() // 更新文档时跳过 try { // 原子操作:获取并递增计数器,不存在则自动创建 const counter = await Counter.findOneAndUpdate( { _id: 'pageCounter' }, { $inc: { seq: 1 } }, // 每次递增1 { new: true, upsert: true } ) // 从0开始计数:第一次递增后seq为1,减1得到初始值0 this.pageIndex = counter.seq - 1 next() } catch (err) { next(err) } }) PageSchema.set('versionKey', 'version') PageSchema.plugin(updateIfCurrentPlugin) PageSchema.statics.build = (attrs: PageAttrs) => { return new Page({ name: attrs.name }) } const Page = mongoose.model<PageDoc, PageModel>('Page', PageSchema) export { Page, Counter }
关键细节说明
- 并发安全:
findOneAndUpdate是MongoDB原子操作,多个请求同时触发也会保证计数唯一,不会重复 - 仅新增生效:通过
this.isNew判断,更新现有文档时不会改动计数器 - 初始值调整:如果要从1开始计数,直接赋值
counter.seq即可,不需要减1
内容的提问来源于stack exchange,提问作者ShadowGames
相关产品推荐
相关产品推荐

