MongoDB 更新操作符

上一篇 CRUD 里反复强调过:updateOne 默认是"整体替换"原文档,通常不是你想要的。要做到"只改某几个字段、给某个数字加一、往数组里加元素"这种精细操作,必须用 $ 开头的更新操作符。这一篇把常用更新操作符系统过一遍。

1. 字段级操作符

这组操作符处理"普通字段"——设值、删字段、改名、自增、自乘、取极值、设当前时间。

// $set:设置字段值(字段不存在则创建)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $set: { views: 100, "meta.likes": 50 } }
)

// $unset:删除字段(值随意给个空串就行)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $unset: { draftNote: "" } }
)

// $rename:改字段名(老 -> 新)
db.posts.updateMany({}, { $rename: { createdAt: "publishedAt" } })

// $inc:数字字段自增(负数就是减),做计数器神器
db.posts.updateOne(
  { title: "我的第一篇" },
  { $inc: { views: 1 } }       // views + 1
)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $inc: { views: -5, "meta.likes": 2 } }   // 多字段同时
)

// $mul:数字字段相乘(字段不存在时按 0 处理)
db.products.updateOne({ name: "纸巾" }, { $mul: { stock: 2 } })

// $min / $max:仅当新值更小/更大才更新(记录"最好成绩")
db.scores.updateOne(
  { player: "张三" },
  { $min: { bestTime: 95.5 } }    // 只有比当前更小才更新
)

// $currentDate:设为当前时间(类型默认 date)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $currentDate: { updatedAt: true } }
)

高频两个:$set(设值,最常用)和 $inc(自增,做计数器神器,如页面浏览量 +1)。$min / $max 在记录"最好/最差成绩"场景特别巧妙——只有新值更优才更新,避免了先查后比再写三步操作。

2. 数组操作符

MongoDB 文档常含数组字段(tags、comments 等),数组操作符是日常更新里用得最多的一组。区别 $push$addToSet 是面试高频题

// $push:往数组末尾加一个元素(可重复)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $push: { tags: "MongoDB" } }
)

// 配合 $each 一次加多个
db.posts.updateOne(
  { title: "我的第一篇" },
  { $push: { tags: { $each: ["NoSQL", "数据库"] } } }
)

// $each + $position:从指定位置插入
db.posts.updateOne(
  { title: "我的第一篇" },
  { $push: { tags: { $each: ["置顶"], $position: 0 } } }
)

// $each + $slice:限制数组长度(只保留最新 N 条)
db.logs.updateOne(
  { user: "u1" },
  { $push: { events: { $each: ["新事件"], $slice: -100 } } }   // 保留最后 100 条
)

// $addToSet:加入元素,但去重(已存在则不动)
db.posts.updateOne(
  { title: "我的第一篇" },
  { $addToSet: { tags: "MongoDB" } }       // 重复加不会变多
)
// 也可以 $addToSet + $each 批量去重加入

// $pull:删除数组中所有匹配条件的元素
db.posts.updateOne(
  { title: "我的第一篇" },
  { $pull: { tags: "笔记" } }              // 把 "笔记" 移出 tags
)
db.posts.updateMany(
  {},
  { $pull: { comments: { visible: false } } }  // 删掉不可见的评论
)

// $pullAll:删除数组中等于指定值(任意一个)的元素
db.posts.updateOne(
  { title: "我的第一篇" },
  { $pullAll: { tags: ["旧标签1", "旧标签2"] } }
)

// $pop:删数组首尾元素(1 末尾,-1 头部)
db.posts.updateOne({ title: "我的第一篇" }, { $pop: { tags: 1 } })

关键区别:

$slice$push 是个生产级技巧——常用于"日志数组只保留最近 N 条",自动滚动淘汰老数据,既避免文档无限膨胀,又免去了单独清理。

3. 嵌套字段更新:点表示法

文档里嵌套对象时,用点表示法精确更新内层字段,不需要把整个外层对象重写。

// 文档里有嵌套对象时,用"点表示法"精确更新内层字段
// 文档结构:
// {
//   title: "...",
//   author: { name: "小明", score: { views: 0, likes: 0 } },
//   tags: ["a"]
// }

// 改 author.name
db.posts.updateOne(
  { _id: ObjectId("...") },
  { $set: { "author.name": "小红" } }
)

// 改嵌套的嵌套 author.score.likes
db.posts.updateOne(
  { _id: ObjectId("...") },
  { $inc: { "author.score.likes": 1 } }
)

// 给嵌套数组里"某个元素"加字段:用 $ 标识符匹配
// 比如给 comments 数组里 name="张三" 的那条加 liked: true
db.posts.updateOne(
  { _id: ObjectId("..."), "comments.name": "张三" },
  { $set: { "comments.$.liked": true } }
)

// 改所有匹配元素用 $[]
db.posts.updateOne(
  { _id: ObjectId("...") },
  { $set: { "comments.$[].visible": true } }   // 所有评论都设为可见
)

最强大也最容易迷惑的是位置操作符 $:它代表"查询条件里匹配到的那个数组元素"。比如查询条件写了 "comments.name": "张三",那么 comments.$.liked 就指代张三那条评论的 liked 字段。要更新所有匹配元素则用 $[]

4. upsert:更新或插入

upsert(update + insert)是个高频技巧——找不到匹配文档就插入一条新的。非常适合做"幂等写入"。

// upsert:找不到匹配就插入(更新或插入,二选一)
// 第三个参数 { upsert: true }
db.counters.updateOne(
  { name: "page_views" },
  { $inc: { count: 1 } },
  { upsert: true }
)
// 第一次执行:没有 name="page_views" 文档 -> 插入 { name, count: 1 }
// 第二次执行:已存在 -> count + 1 -> { name, count: 2 }

// $setOnInsert:仅在"插入"时设的字段(更新时不动)
db.counters.updateOne(
  { name: "page_views" },
  {
    $inc: { count: 1 },
    $setOnInsert: { createdAt: new Date() }    // 只有新建时才写 createdAt
  },
  { upsert: true }
)

// findOneAndUpdate:更新并返回文档(可指定返回前/后)
const doc = db.posts.findOneAndUpdate(
  { title: "我的第一篇" },
  { $inc: { views: 1 } },
  { returnNewDocument: true }    // 返回更新后的文档
)

典型场景:统计页面访问数。第一次访问时文档不存在,upsert 会插入 count: 1;后续访问则 $inc 累加。配合 $setOnInsert 还能让"创建时间"只在第一次插入时写入,后续更新不动它——保持数据语义干净。

5. 原子性与 findOneAndUpdate

MongoDB 对单个文档的更新是原子的——意思是 $inc 这种"读出旧值、加一、写回"的操作不会被并发请求打断,你可以放心做计数器,不用担心并发下丢更新。

如果更新后还要拿到新值,用 findOneAndUpdate 一步搞定(替代"先 update 再 find"两步),且原子。类似还有 findOneAndReplacefindOneAndDelete

6. 批量更新:updateMany

updateMany 改所有匹配文档——做"批量修改"时千万别忘了用 Many 版本(默认 updateOne 只改第一条)。

7. 更新操作符速查表

8. 常见踩坑

小结

这一篇覆盖了 MongoDB 的更新操作符:字段级的 $set/$inc/$unset、数组级的 $push/$addToSet/$pull、嵌套更新的点表示法和位置操作符 $,还有高频的 upsert。从此你能精准修改文档的任意角落。下一篇进入 MongoDB 的另一个杀手锏——聚合管道(aggregation pipeline)

← 上一篇 MongoDB 查询操作符

下一篇 MongoDB 聚合管道

✈️💬