Mongoose为已有users集合新增自增userNumber字段及旧数据填充方法
现有Users集合实现userNumber自增与存量数据补全的落地方案
代码存放位置说明
所有和Schema、模型定义相关的代码,全部放在你项目中存放原有User模型的models目录下即可,不需要额外调整目录结构,业务层/路由层代码完全不需要改动,沿用原有的User.create()方法就会自动触发自增逻辑。
自增能力的核心实现
- 第一步:在models目录下新建
counter.model.js,定义专门的计数集合,用来存储每个自增序列的当前最大值,通过MongoDB原子操作避免并发下的编号重复问题
const mongoose = require('mongoose'); const counterSchema = new mongoose.Schema({ seqName: { type: String, required: true, unique: true }, seqValue: { type: Number, required: true, default: 0 } }); module.exports = mongoose.model('Counter', counterSchema);
- 第二步:修改原有User Schema,新增userNumber字段,并绑定预保存钩子实现自动赋值
const mongoose = require('mongoose'); const Counter = require('./counter.model'); const userSchema = new mongoose.Schema({ // 原有字段完整保留,不做修改 userType: { type: String, required: false, }, uid: { type: String, required: false, }, firstName: { type: String, required: true, }, lastName: { type: String, }, email: { type: String, lowercase: true, }, // 新增自增字段 userNumber: { type: Number, unique: true } }); // 预保存钩子,仅对新创建的文档触发自增逻辑 userSchema.pre('save', async function(next) { // 跳过非新文档、已经手动指定了userNumber的文档的自增流程 if (!this.isNew || this.userNumber) return next(); try { // 原子操作更新计数器,不存在对应序列条目时自动创建 const counter = await Counter.findOneAndUpdate( { seqName: 'userNumber' }, { $inc: { seqValue: 1 } }, { new: true, upsert: true } ); this.userNumber = counter.seqValue; next(); } catch (err) { next(err); } }); module.exports = mongoose.model('User', userSchema);
存量历史文档批量补全userNumber的操作
- 第一步:在项目
scripts目录下新建一次性执行脚本fill-user-number.js,用来给老文档批量赋值
const mongoose = require('mongoose'); const User = require('../models/user.model'); const Counter = require('../models/counter.model'); // 替换为你项目的MongoDB连接字符串 mongoose.connect('mongodb://localhost:27017/你的数据库名').then(async () => { try { // 按MongoDB _id正序查询所有没有userNumber字段的老文档,_id自带时间戳属性,等价于按创建时间从早到晚排序 const oldUsers = await User.find({ userNumber: { $exists: false } }) .sort({ _id: 1 }) .lean(); console.log(`共找到${oldUsers.length}条待补值的历史文档`); // 逐文档按顺序赋值userNumber,从1开始递增 for (let i = 0; i < oldUsers.length; i++) { await User.updateOne( { _id: oldUsers[i]._id }, { $set: { userNumber: i + 1 } } ); } // 同步计数器的当前最大值,避免后续新文档的编号和存量值重复 await Counter.findOneAndUpdate( { seqName: 'userNumber' }, { $set: { seqValue: oldUsers.length } }, { upsert: true } ); console.log('存量文档补全完成,自增序列已同步'); process.exit(0); } catch (err) { console.error('执行出错:', err); process.exit(1); } });
- 第二步:先对Users集合做全量备份,再执行上述脚本完成补全
- 第三步:待所有老文档的userNumber赋值完成后,再为userNumber字段创建唯一索引,避免补值过程中存在空值导致索引创建失败
常见踩坑提醒
- 预保存钩子一定要加
isNew判断,否则后续更新老用户信息时,每次触发save流程都会把userNumber重新覆盖 - 批量补值时一定要用
updateOne直接写库,不要走findOne+save的流程,否则会触发预保存钩子从计数器拿值,造成编号冲突 - 计数器同步步骤不能省略,否则后续新创建用户时,自增的编号会从1开始和存量值重复
内容的提问来源于stack exchange,提问作者Mani
相关产品推荐
相关产品推荐

