
本文介绍如何利用 mgo 的 Query.Apply 方法,通过单次 findAndModify 操作完成文档更新与获取,避免先 Update 再 Find 的两次往返,提升性能与数据一致性。
本文介绍如何利用 mgo 的 `query.apply` 方法,通过单次 `findandmodify` 操作完成文档更新与获取,避免先 `update` 再 `find` 的两次往返,提升性能与数据一致性。
在 MongoDB 应用开发中,一个常见模式是:先对某文档执行部分字段更新(如递增计数器、修改状态),再立即读取其完整内容以供后续逻辑使用。若采用传统方式——先调用 Update() 或 Upsert(),再执行 Find().One()——不仅增加一次网络往返开销,更存在竞态风险:中间可能被其他操作修改,导致读到过期或不一致的数据。
幸运的是,mgo 提供了 Query.Apply() 方法,底层封装 MongoDB 原生的 findAndModify 命令,支持原子性地执行更新(或插入/删除)并直接返回修改前或修改后的完整文档,完美解决上述问题。
核心用法:mgo.Change 与 Apply
Apply 接收一个 mgo.Change 结构体,用于声明变更行为。关键字段包括:
- Update: 指定更新操作符(如 $set, $inc, $push 等);
- ReturnNew: 若为 true,返回更新后的文档;若为 false(默认),返回更新前的文档;
- Upsert: 若为 true 且匹配文档不存在,则插入新文档;
- Remove: 若为 true,则执行删除并返回被删文档(此时 Update 被忽略)。
以下是一个典型示例,对 _id 为 id 的文档原子性地递增字段 n,并获取更新后的完整文档:
var doc MyDocument // 假设已定义结构体,含字段 N int
change := mgo.Change{
Update: bson.M{"$inc": bson.M{"n": 1}},
ReturnNew: true,
}
info, err := col.Find(bson.M{"_id": id}).Apply(change, &doc)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Updated document: %+v\n", doc) // 此时 doc 已包含所有字段(含未修改字段)
fmt.Printf("New value of n: %d\n", doc.N)
✅ 优势说明:
- 仅一次数据库交互,降低延迟与连接压力;
- 原子性保障:匹配、修改、返回三步不可分割,杜绝并发干扰;
- 自动填充:即使 Update 只修改部分字段,&doc 仍会接收服务端返回的完整文档快照(含原始未变字段),无需额外 Find()。
注意事项
- Apply 要求查询条件(Find(...))必须能唯一匹配目标文档(如使用 _id),否则行为未定义(MongoDB 仅允许对单个文档执行 findAndModify);
- 若匹配文档不存在且未设置 Upsert: true,Apply 将返回 info.Updated == 0 和 err == nil,但 &doc 不会被赋值(保持零值)——务必检查 info.Updated 或 err;
- ReturnNew: true 时,若发生 Upsert,返回的是新插入的文档(含服务端生成的 _id 等);
- mgo.v2 已归档,生产环境建议评估迁移至官方驱动 mongo-go-driver,其对应方法为 Collection.FindOneAndUpdate,语义更清晰、维护更活跃。
综上,Query.Apply 是 mgo 中实现“更新即获取”的高效、安全方案。合理运用它,可显著简化逻辑、增强健壮性,并为高并发场景提供可靠的数据一致性保证。











