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

使用MongoDB C#驱动通过$addToSet更新数组内文档(Upsert需求)

解决MongoDB C#驱动中文档数组的Upsert问题

这个问题我之前也碰到过!因为$addToSet是判断整个子文档是否完全一致才会跳过添加,但你要的是根据DateTime(也就是序列化后的k字段)来判断,所以直接用它肯定不行。给你几个可行的方案,最推荐的是用MongoDB的更新管道来实现原子性的Upsert操作,既处理数组元素的更新/添加,也能处理整个文档不存在的情况:

核心思路:用更新管道实现原子Upsert

因为你把Dictionary<DateTime, double>序列化成了ArrayOfDocuments(也就是[{k: 日期, v: 数值}, ...]的结构),我们可以通过MongoDB的更新管道,分两步完成需求:

  1. 遍历数组,更新已存在的匹配元素(根据k字段等于目标DateTime)
  2. 检查数组中是否存在匹配元素,不存在则添加新的子文档
  3. 同时开启文档级的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 }
);

代码逻辑解释

  1. $map遍历更新:通过$map遍历Contain数组,找到k等于目标日期的元素,用$mergeObjects更新它的v值,其他元素保持不变。
  2. 条件判断添加:用$filter统计匹配元素的数量,如果数量为0且数组不为null,就用$concatArrays把新元素添加到数组末尾;如果是刚创建的文档(Contain为null),则直接初始化数组包含目标元素。
  3. 文档级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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:18:07