Mongoose模型字段自增实现求助——MySQL转MongoDB迁移遇错
解决MongoDB中Mongoose自增字段的重复键问题
我来帮你逐个排查两个方案的问题,再给你修正后的可行方案:
方案1:mongoose-auto-increment插件报错原因
你碰到的E11000 duplicate key error主要是两个问题导致的:
- 连接上下文不一致:你用
mongoose.createConnection创建了独立连接,但如果你的Emborder模型是通过mongoose.model()(默认全局连接)创建的,插件初始化在单独连接上就无法正确跟踪模型的ID序列,导致生成的ID和现有数据重复。 - 计数器序列与现有数据冲突:你设置了
startAt: 28855,但数据库的counters集合(插件自动创建的序列跟踪集合)里,对应Emborder的记录可能保存了错误的序列值,或者现有数据中已经存在插件生成的ID。
修正方案1
确保模型和插件使用同一个连接:
var mongoose = require('mongoose'), Schema = mongoose.Schema, autoIncrement = require('mongoose-auto-increment'); // 统一使用同一个连接 var connection = mongoose.createConnection("mongodb://localhost/myDatabase"); autoIncrement.initialize(connection); // 通过当前连接创建模型,而非全局mongoose.model var EmborderSchema = new Schema({ /* 你的字段定义 */ }); EmborderSchema.plugin(autoIncrement.plugin, { model: 'Emborder', field: 'emborder_id', startAt: 28855, incrementBy: 1 }); var Emborder = connection.model('Emborder', EmborderSchema);
另外,手动检查数据库中的counters集合,如果有Emborder对应的记录,把它的seq值改成当前集合中最大的emborder_id,避免序列冲突。
方案2:pre save钩子报错原因
这个方案有三个致命问题:
- this指向错误:
findOne的exec回调里,this不再指向当前要保存的文档,而是回调函数的上下文(严格模式下为undefined),所以this.emborder_id = data.emborder_id + 1根本没赋值到文档上,导致emborder_id还是undefined,MongoDB会把它转为null,而你给该字段加了唯一索引,自然触发null重复的错误。 - 未处理空集合场景:如果集合里还没有任何数据,
data会是null,此时data.emborder_id会直接报错,导致next()无法执行,文档也无法正常保存。 - 并发安全漏洞:高并发场景下,多个请求同时执行
findOne查询最大ID,会得到相同的最大值,进而生成重复的自增ID。
修正方案2(原子操作版,解决并发问题)
用MongoDB的原子操作(findOneAndUpdate)维护一个独立的计数器集合,确保每次自增都是原子性的,彻底避免并发冲突:
首先创建计数器模型:
var CounterSchema = new Schema({ model: { type: String, required: true, unique: true }, seq: { type: Number, default: 0 } }); var Counter = connection.model('Counter', CounterSchema);
然后修改pre save钩子(用async/await避免this指向问题):
EmborderSchema.pre('save', async function(next) { // 仅当emborder_id未定义时生成自增ID if (this.emborder_id == undefined) { try { // 原子操作:找到对应模型的计数器,自增seq值 const counter = await Counter.findOneAndUpdate( { model: 'Emborder' }, { $inc: { seq: 1 } }, { upsert: true, new: true, setDefaultsOnInsert: true } ); // 这里的this正确指向当前文档 this.emborder_id = counter.seq; next(); } catch (err) { next(err); } } else { next(); } });
初始化起始值
如果你需要从28855开始自增,在应用启动时执行一次初始化:
// 把计数器初始值设为28854,这样第一次自增后就是28855 Counter.updateOne( { model: 'Emborder' }, { seq: 28854 }, { upsert: true } ).catch(err => console.error('初始化计数器失败:', err));
这个方案既解决了this指向和空集合的问题,又通过原子操作保证了高并发场景下不会生成重复ID。
内容的提问来源于stack exchange,提问作者Sunil Jhamnani
相关产品推荐
相关产品推荐

