Node.js Mongoose实现auto increment报错及自增值恒为1问题
Mongoose实现自定义字段自增的正确方案
原有实现的问题说明
- 第一版报错原因:MongoDB Node.js驱动及Mongoose早已废弃
Collection#findAndModify接口,该能力已由findOneAndUpdate完全替代,直接调用旧接口会抛出「unimplemented by driver」错误。同时该实现未单独维护全局序列计数,直接在业务集合内操作序列文档,会造成业务数据和计数数据混存。 - 第二版id恒为1的原因:逻辑存在本质错误。实现中先插入无id字段的新文档,再对该单条文档执行
$inc: {id: 1}操作;由于新文档不存在id字段,$inc会从0开始计算,因此每条文档执行后id值均为1,完全没有实现全局连续计数。
标准实现方案(MongoDB官方推荐)
核心思路是单独维护一个全局计数集合,通过原子操作获取递增序列值,再赋值给新插入的业务文档,并发场景下也能保证id唯一且连续递增。
- 新建独立的
counters集合,专门存储各业务字段的序列值,不与业务数据混存。集合中单条文档结构为{ _id: 序列标识名, sequence_value: 当前计数值 } - 每次插入业务文档前,通过原子操作
findOneAndUpdate+$inc对对应序列的计数值加1,拿到更新后的最新值 - 将拿到的序列值赋值给新业务文档的自增字段,再执行插入操作
可直接运行的代码示例
import mongoose, { Schema, Document } from 'mongoose'; // 定义计数器集合Model interface CounterDoc extends Document { _id: string; sequence_value: number; } const CounterSchema = new Schema<CounterDoc>({ _id: { type: String, required: true }, sequence_value: { type: Number, default: 0 } }); const Counter = mongoose.model('Counter', CounterSchema); // 定义业务Todo集合Model interface TodoDoc extends Document { id: number; todoText: string; } const TodoSchema = new Schema<TodoDoc>({ id: { type: Number, unique: true, index: true }, // 自增字段加唯一索引避免重复 todoText: { type: String, required: true } }); const Todo = mongoose.model('Todo', TodoSchema); /** * 原子操作获取指定序列的下一个自增值 * @param sequenceName 序列唯一标识名 */ const getNextSequence = async (sequenceName: string): Promise<number> => { const res = await Counter.findOneAndUpdate( { _id: sequenceName }, { $inc: { sequence_value: 1 } }, { new: true, // 返回更新后的最新值 upsert: true, // 序列不存在时自动创建初始文档 setDefaultsOnInsert: true } ); return res.sequence_value; }; // 插入带自增id的业务文档 const addTodo = async (todoText: string) => { // 先获取全局自增id,再插入文档 const nextId = await getNextSequence('todoId'); return Todo.create({ id: nextId, todoText }); };
优化:通过Mongoose预处理钩子自动注入自增id
如果不想每次插入文档前手动调用序列获取方法,可以将逻辑注册到Schema的pre('save')钩子中,新文档保存时会自动赋值自增id:
TodoSchema.pre('save', async function (next) { // 仅对新插入、且未手动设置id的文档生成自增值 if (this.isNew && !this.id) { this.id = await getNextSequence('todoId'); } next(); }); // 注册钩子后直接保存即可,无需手动查序列 // const newTodo = new Todo({ todoText: '测试待办' }); // await newTodo.save(); // 保存完成后自动带有连续递增的id值
避坑说明
- 不要使用「先插入文档再给单条文档执行$inc」的逻辑,该方式没有全局计数,无法实现跨文档的连续自增
- 序列获取操作必须开启
upsert: true,否则第一次插入业务文档时会因为找不到序列文档报错 - 由于MongoDB单文档操作是原子性的,该方案在并发插入场景下也不会出现id重复问题,无需额外加锁
- 多个业务集合的自增字段可以共用同一个
counters集合,只要给不同字段设置不同的序列标识名即可(比如用户自增id用userId、订单自增id用orderId)
内容的提问来源于stack exchange,提问作者Inspiraller
相关产品推荐
相关产品推荐

