使用MongoDB C#驱动通过$addToSet更新数组内文档(Upsert需求)
解决MongoDB C#驱动中文档数组的Upsert问题
这个问题我之前也碰到过!因为$addToSet是判断整个子文档是否完全一致才会跳过添加,但你要的是根据DateTime(也就是序列化后的k字段)来判断,所以直接用它肯定不行。给你几个可行的方案,最推荐的是用MongoDB的更新管道来实现原子性的Upsert操作,既处理数组元素的更新/添加,也能处理整个文档不存在的情况:
核心思路:用更新管道实现原子Upsert
因为你把Dictionary<DateTime, double>序列化成了ArrayOfDocuments(也就是[{k: 日期, v: 数值}, ...]的结构),我们可以通过MongoDB的更新管道,分两步完成需求:
- 遍历数组,更新已存在的匹配元素(根据
k字段等于目标DateTime) - 检查数组中是否存在匹配元素,不存在则添加新的子文档
- 同时开启文档级的Upsert,确保如果
TimeSeries文档本身不存在,会自动创建并添加目标元素
C#代码实现
using MongoDB.Bson; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Threading.Tasks; internal class TimeSeries { [BsonId] internal string Name { get; set; } [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] internal Dictionary<DateTime, double> Contain { get; set; } } // 假设你已经初始化了集合实例 var collection = mongoDatabase.GetCollection<TimeSeries>("TimeSeries"); // 要Upsert的目标日期和数值 var targetDate = new DateTime(2024, 5, 20); var targetValue = 123.45; // 构建更新管道 var updatePipeline = new BsonDocument[] { // 第一步:更新数组中已存在的匹配元素 new BsonDocument("$set", new BsonDocument("Contain", new BsonDocument("$map", new BsonDocument { { "input", "$Contain" }, { "as", "item" }, { "in", new BsonDocument("$cond", new BsonArray { // 判断当前元素的k是否等于目标日期 new BsonDocument("$eq", new BsonArray { "$$item.k", targetDate }), // 匹配的话,合并原元素和新的v值 new BsonDocument("$mergeObjects", new BsonArray { "$$item", new BsonDocument("v", targetValue) }), // 不匹配则保留原元素 "$$item" }) })) ), // 第二步:如果数组中没有匹配元素,添加新的子文档 new BsonDocument("$set", new BsonDocument("Contain", new BsonDocument("$cond", new BsonArray { // 判断是否存在匹配元素,或者文档刚创建时Contain为null new BsonDocument("$or", new BsonArray { new BsonDocument("$gt", new BsonArray { new BsonDocument("$size", new BsonDocument("$filter", new BsonDocument { { "input", "$Contain" }, { "cond", new BsonDocument("$eq", new BsonArray { "$$this.k", targetDate }) } })), 0 }), new BsonDocument("$eq", new BsonArray { "$Contain", BsonNull.Value }) }), // 如果存在匹配元素,或者Contain为null(新文档): new BsonDocument("$cond", new BsonArray { new BsonDocument("$eq", new BsonArray { "$Contain", BsonNull.Value }), // 新文档则初始化数组为包含目标元素 new BsonArray { new BsonDocument("k", targetDate).Add("v", targetValue) }, // 已有文档则保留更新后的数组 "$Contain" }), // 不存在匹配元素则拼接新元素到数组 new BsonDocument("$concatArrays", new BsonArray { "$Contain", new BsonArray { new BsonDocument("k", targetDate).Add("v", targetValue) } }) }) )) }; // 执行更新操作,开启Upsert var updateResult = await collection.UpdateOneAsync( Builders<TimeSeries>.Filter.Eq(t => t.Name, "你的文档ID"), updatePipeline, new UpdateOptions { IsUpsert = true } );
代码逻辑解释
- $map遍历更新:通过
$map遍历Contain数组,找到k等于目标日期的元素,用$mergeObjects更新它的v值,其他元素保持不变。 - 条件判断添加:用
$filter统计匹配元素的数量,如果数量为0且数组不为null,就用$concatArrays把新元素添加到数组末尾;如果是刚创建的文档(Contain为null),则直接初始化数组包含目标元素。 - 文档级Upsert:
IsUpsert = true确保如果找不到匹配的TimeSeries文档,会自动创建一个新文档,并完成数组元素的初始化。
备选方案:先查询再更新(不推荐,有并发风险)
如果你的场景没有并发写入的问题,也可以先查询文档是否存在,再判断数组中是否有目标元素,然后决定是更新还是添加:
var filter = Builders<TimeSeries>.Filter.Eq(t => t.Name, "你的文档ID"); var document = await collection.Find(filter).FirstOrDefaultAsync(); if (document == null) { // 文档不存在,创建新文档并添加元素 await collection.InsertOneAsync(new TimeSeries { Name = "你的文档ID", Contain = new Dictionary<DateTime, double> { { targetDate, targetValue } } }); } else { if (document.Contain.ContainsKey(targetDate)) { // 元素存在,更新值 var update = Builders<TimeSeries>.Update.Set(t => t.Contain[targetDate], targetValue); await collection.UpdateOneAsync(filter, update); } else { // 元素不存在,添加新元素 var update = Builders<TimeSeries>.Update.AddToSet(t => t.Contain, new KeyValuePair<DateTime, double>(targetDate, targetValue)); await collection.UpdateOneAsync(filter, update); } }
⚠️ 注意:这个方案不是原子操作,如果有多个线程同时写入,可能会出现重复添加或者数据覆盖的问题,所以只适合低并发场景。
内容的提问来源于stack exchange,提问作者SockenLoch
相关产品推荐
相关产品推荐

