更新操作符必须全部放在 update document 参数中,如 $set 和 $inc 需同层写入;$inc 要求字段为数字类型且需预先存在;嵌套字段更新须用点号路径,避免整体替换。

更新操作符必须全部放在 update document 参数里
很多人把 $inc 和 $set 拆开放进不同参数位置,比如误把 $push 当作 options 传——$inc 同样会静默失效。Mongoose 的 updateOne、updateMany 等方法只接受一个 update document 对象作为第二个参数,所有操作符都得塞进去。
常见错误写法:
await User.updateOne(
{ _id: userId },
{ $set: { name: 'Alice' } },
{ $inc: { loginCount: 1 } } // ❌ 第三个参数是 options,$inc 被忽略
)
正确写法:
await User.updateOne(
{ _id: userId },
{
$set: { name: 'Alice' },
$inc: { loginCount: 1 }
}
)
- 多个操作符可共存,顺序无关
- 不要在 update document 外套一层对象(如
{ update: { $set: ... } }) - 如果同时要设置普通字段和操作符字段,统一写在同一个对象里,例如
{ name: 'Alice', $inc: { score: 10 } }
注意 $inc 的类型安全与初始值问题
$inc 要求目标字段必须是数字类型,否则 MongoDB 会抛出 Path xxx must be a number 错误。它不会自动初始化为 0,也不会做类型转换。
典型踩坑场景:
- 字段首次不存在 →
$inc失败(MongoDB 不允许对 undefined 字段执行增量) - 字段存在但值是字符串(如
"5")→ 报错,不隐式转成数字 - 字段是 null → 同样报错
稳妥做法是配合 $setOnInsert 或前置初始化:
await User.findOneAndUpdate(
{ _id: userId },
{
$set: { lastActive: new Date() },
$inc: { loginCount: 1 }
},
{ upsert: true, setDefaultsOnInsert: true }
)
或者更明确地初始化字段(推荐用于关键计数器):
await User.updateOne(
{ _id: userId, loginCount: { $exists: false } },
{ $set: { loginCount: 0 } }
)
// 再执行 $inc
并发下 $inc 的原子性是可靠的,但业务逻辑仍需防护
MongoDB 层面保证单文档内 $inc 是原子的,多个并发请求不会导致计数丢失。这点比应用层自增 + save 安全得多。
但要注意边界情况:
-
$inc本身不校验业务规则,比如“不能负增长”或“不能超过上限”,需靠 schema validator 或 pre-save hook 补充 - 若更新条件涉及其他字段(如
{ status: 'active', loginCount: { $lt: 100 } }),要确认 matchedCount > 0,否则$inc根本没执行 - 乐观锁场景下,如果用了
versionKey(默认__v),记得用$inc: { __v: 1 },且必须检查返回的matchedCount,不是modifiedCount
嵌套字段更新时别混淆 $set 和整体替换
想更新 profile.stats.totalTime,直接写 $set: { 'profile.stats': { totalTime: 120 } } 会把整个 stats 对象替换成新值,丢掉其他字段(如 lastUpdated)。
正确方式永远是点号路径 + $set:
await User.updateOne(
{ _id: userId },
{ $set: { 'profile.stats.totalTime': 120 } }
)
如果要批量更新多个嵌套字段,同样展开写:
{
$set: {
'profile.stats.totalTime': 120,
'profile.stats.lastUpdated': new Date()
}
}
切记:$inc 也支持点号路径(如 $inc: { 'profile.stats.visits': 1 }),但不支持数组索引通配(stats.0.visits 需用 $ 或 $[] 等专门操作符)。

















