You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go操作MongoDB更新字段时将旧值$push到历史数组实现版本控制

实现方案

普通MongoDB更新语法无法直接在单条语句中读取文档原有字段值作为更新参数,要实现「更新字段时将旧值存入历史数组」的需求,有两种成熟方案,优先选择第一种原子实现。

方案1:聚合管道更新(MongoDB 4.2+ 支持,推荐)

MongoDB 4.2及以上版本支持将聚合管道作为更新参数,管道内可以直接通过$字段名引用文档更新前的原始值,单条语句原子完成所有操作,无并发一致性问题,性能最优。

注意:聚合管道更新阶段使用聚合操作符,不能直接使用普通更新的$push,可以用$concatArrays实现完全一致的数组追加效果。

示例代码:

func UpdateDocument(ctx context.Context, id uuid.UUID, updateFields bson.M) error {
	filter := bson.M{"_id": id}

	// 构建聚合管道更新语句
	update := mongo.Pipeline{
		{{
			Key: "$set",
			Value: bson.D{
				// 先拼接所有待更新的字段,若updateFields是动态传入的,可以遍历键值对自动拼接
				{"original_name", updateFields["original_name"]},
				{"user_name", updateFields["user_name"]},
				// 追加original_name旧值到历史数组:兼容历史数组不存在的场景,初始化为空数组
				{"old_original_names", bson.D{
					{"$concatArrays", bson.A{
						bson.D{{"$ifNull", bson.A{"$old_original_names", bson.A{}}}},
						bson.A{"$original_name"}, // 这里$original_name就是更新前的原字段值
					}},
				}},
				// 版本号+1,兼容首次无version字段的场景,默认从1开始计数
				{"version", bson.D{
					{"$add", bson.A{
						bson.D{{"$ifNull", bson.A{"$version", 0}}},
						1,
					}},
				}},
			},
		}},
	}

	_, err := r.db.FindOneAndUpdate(ctx, filter, update).DecodeBytes()
	return err
}

如果需要追踪多个字段的历史值,只需要在$set块中追加对应字段的数组拼接逻辑即可,比如要追踪user_name的变更历史,就新增如下配置:

{"old_user_names", bson.D{
	{"$concatArrays", bson.A{
		bson.D{{"$ifNull", bson.A{"$old_user_names", bson.A{}}}},
		bson.A{"$user_name"},
	}},
}},

方案2:读取旧文档后追加(兼容MongoDB 4.2以下版本)

如果使用的MongoDB版本低于4.2不支持聚合管道更新,可以通过FindOneAndUpdate配置返回更新前的文档,取出旧值后再追加到历史数组。该方案需要两次数据库交互,非原子,必须配合乐观锁避免并发问题。

示例代码:

func UpdateDocument(ctx context.Context, id uuid.UUID, obj interface{}, currentVersion int) error {
	// 把version加入过滤条件做乐观锁,避免并发更新冲突
	filter := bson.M{"_id": id, "version": currentVersion}
	// 配置返回更新前的文档
	opts := options.FindOneAndUpdate().SetReturnDocument(options.Before)

	var oldDoc bson.M
	// 第一步:更新字段、递增版本,拿到更新前的旧文档
	err := r.db.FindOneAndUpdate(ctx, filter, bson.M{
		"$set": obj,
		"$inc": bson.M{"version": 1},
	}, opts).Decode(&oldDoc)
	if err != nil {
		return err
	}

	// 第二步:从旧文档取出原值,追加到历史数组
	if oldVal, exists := oldDoc["original_name"]; exists {
		_, err = r.db.UpdateOne(ctx, bson.M{"_id": id}, bson.M{
			"$push": bson.M{"old_original_names": oldVal},
		})
		if err != nil {
			return err
		}
	}

	return nil
}

该方案注意事项:

  • 两次写操作非原子,如果第一步更新成功、第二步追加历史失败会导致数据不一致,建议添加失败重试或补偿逻辑
  • 必须给version字段加索引,更新时携带当前版本号做乐观锁校验,否则并发场景下会出现旧值和版本对应错乱的问题
  • 性能比方案1差30%以上,仅在版本不支持时使用

内容的提问来源于stack exchange,提问作者dante0x75

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 03:12:14