必须用 $set 操作符,直接传整个文档会删掉未显式写出的字段;mongodb updateone 等默认整文档替换,仅当 update 参数含 $ 开头操作符才执行局部更新。

必须用 $set 操作符,直接传整个文档会删掉没显式写出的字段——这不是 bug,是 MongoDB 默认行为。
为什么直接赋值会丢字段?
MongoDB 的 updateOne、updateMany 等方法,默认执行的是“整文档替换”。只要 update 参数里不含 $ 开头的操作符,驱动就认为你要换掉整个文档。
- 比如文档是
{ _id: 1, name: "Alice", email: "a@b.com", status: "active" } - 你执行
db.users.updateOne({ _id: 1 }, { name: "Bob" }) - 结果变成
{ _id: 1, name: "Bob" }——email和status全没了
正确做法:只用 $set 更新指定路径
$set 是最常用也最安全的局部更新操作符,它只改你明确列出的字段,其余不动。
- 更新顶层字段:
{ "$set": { "name": "Bob" } } - 更新嵌套字段:
{ "$set": { "profile.phone": "+86138..." } } - 更新数组中某一项(用位置):
{ "$set": { "tags.0": "urgent" } } - 更新数组中匹配项(用
$[i]):{ "$set": { "items.$[i].qty": 5 } },配合arrayFilters: [{ "i.name": "apple" }]
Go 里动态构建 $set 要小心 nil 指针
REST API 做 PATCH 更新时,常只传部分字段。Go 结构体用指针字段(如 *string)能区分“未提供”和“设为空字符串”,但 BSON 序列化默认会把 nil 指针转成 null,导致意外覆盖。
- 错误写法:
updateData := bson.M{"$set": user}(如果user.Email是nil,可能写入"email": null) - 正确做法:手动遍历结构体字段,跳过
nil指针,只把非空值塞进$set映射 - 或用现代驱动(如
go.mongodb.org/mongo-driver/bson)配合自定义 marshaler,避免自动序列化nil
容易被忽略的坑:数组更新和分片键不可变
数组字段更新稍复杂,而分片集合里某些字段改不了。
- 更新数组元素必须用
$或$[identifier],不能写死索引(如list.0)除非你确定位置 - 分片键字段(比如
tenant_id)允许更新,但前提是它不是_id;而_id字段在任何情况下都不能改 - 嵌套太深(>4 层)建议拆到单独集合,否则
$set路径易出错,查询也难优化











