MongoDB更新冲突报错原因、版本兼容及重构方案咨询
问题描述
执行以下基于Mongoose的MongoDB查询:
const myData = await this.findOneAndUpdate({ myId, color, }, { $setOnInsert: { myId, color, date: now.format('YYYY-MM-DD'), counter: myObj.counter - amount, }, $inc: { counter: -amount }, }, { new: true, upsert: true, });
出现报错:
"Updating the path 'count' would create a conflict at 'count'"
注:报错中的count应为代码中的counter,属于笔误。
发现counter同时出现在$setOnInsert和$inc操作中,但不理解冲突原因。该代码在MongoDB 3.4.24 Community版本可正常运行,在MongoDB 5.0.11 Community版本则报错,咨询以下问题:
- 该报错的具体原因是什么?是否属于BUG?
- 为何旧版本MongoDB可正常运行?
- 最优的代码重构方案是什么?
解答
1. 报错原因及是否为BUG
这不是BUG,是MongoDB 4.0及以上版本新增的字段更新冲突校验机制导致的。当upsert: true触发插入操作(目标文档不存在)时,$setOnInsert会给counter赋值,同时$inc也会尝试对同一个字段执行增量操作——两个操作在插入场景下会同时作用于counter,属于对同一字段的重复修改,新版本MongoDB会严格校验这种冲突并抛出错误。
2. 旧版本可正常运行的原因
MongoDB 3.4及更早版本没有这种严格的字段冲突校验逻辑。在插入场景下,它会先执行$setOnInsert的赋值,再执行$inc的增量操作,最终counter的值为(myObj.counter - amount) + (-amount),不会触发报错。但这种行为存在逻辑歧义,所以MongoDB在后续版本中新增了校验规则,避免这类可能引发逻辑错误的操作。
3. 最优代码重构方案
核心是避免插入场景下$setOnInsert和$inc同时操作counter,推荐两种方案:
方案一:聚合管道原子更新(MongoDB 4.2+ 推荐)
利用聚合管道的$cond判断是否为插入操作,原子性完成逻辑,无需多次数据库请求:
const myData = await this.findOneAndUpdate({ myId, color, }, [ { $set: { myId, color, date: { $cond: [{ $eq: ["$_id", null] }, now.format('YYYY-MM-DD'), "$date"] }, counter: { $cond: [ { $eq: ["$_id", null] }, // 新插入文档的_id为null,以此判断 myObj.counter - amount, // 插入时设置初始值 { $subtract: ["$counter", amount] } // 更新时执行减法,等价于$inc: {counter: -amount} ] } } } ], { new: true, upsert: true, });
方案二:分场景执行(兼容低版本)
先查询文档是否存在,再分别执行插入或更新操作:
const doc = await this.findOne({ myId, color }); let myData; if (doc) { myData = await this.findOneAndUpdate( { myId, color }, { $inc: { counter: -amount } }, { new: true } ); } else { myData = await this.create({ myId, color, date: now.format('YYYY-MM-DD'), counter: myObj.counter - amount }); }
内容的提问来源于stack exchange,提问作者PlayHardGoPro
相关产品推荐
相关产品推荐

