You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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唯一且连续递增。

  1. 新建独立的counters集合,专门存储各业务字段的序列值,不与业务数据混存。集合中单条文档结构为{ _id: 序列标识名, sequence_value: 当前计数值 }
  2. 每次插入业务文档前,通过原子操作findOneAndUpdate+$inc对对应序列的计数值加1,拿到更新后的最新值
  3. 将拿到的序列值赋值给新业务文档的自增字段,再执行插入操作

可直接运行的代码示例

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 04:27:26