C# .NET Core 3.1中使用 MongoDB.Driver 更新嵌套數組元素和關聯的一些坑

C# .NET Core 3.1中使用 MongoDB.Driver 更新數組元素和關聯的一些坑

前言:

由於工作的原因,使用的資料庫由原來的 關係型資料庫 MySQL、SQL Server 變成了 非關係型資料庫 MongoDB。可以簡單的理解為存下的是 Json(實際是一個類似的東西叫 Bson)。由於仍然使用 C# 作為開發語言,自然是繞不開官方的資料庫驅動 MongoDB.Driver。由於 MongoDB 的特性和驅動程式自身的實現,也可能是因為個人的習慣,感覺使用起來並不順手,還遇到了很多坑XD,因此記錄一下。

環境與數據

A. 使用 Visual Studio 2019、.NET Core 3.1 和 MongoDB.Driver 2.13.1
B. 類的定義(還是用《原神》了哈哈)
/// <summary>
/// 原神角色
/// </summary>
public class YuanshenRole
{
    /// <summary>
    /// 主鍵
    /// </summary>
    public ObjectId _id { get; set; }
    /// <summary>
    /// 名字
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 元素:火、水、岩、冰、風等
    /// </summary>
    public ObjectId ElementId { get; set; }
    /// <summary>
    /// 所擁有的武器
    /// </summary>
    public List<Weapon> Weapons { get; set; }
}

/// <summary>
/// 元素
/// </summary>
public class Element
{
    /// <summary>
    /// 主鍵
    /// </summary>
    public ObjectId _id { get; set; }
    /// <summary>
    /// 名稱
    /// </summary>
    public string ElementName { get; set; }
}

/// <summary>
/// 武器
/// </summary>
public class Weapon
{
    /// <summary>
    /// 唯一標誌
    /// </summary>
    public string Id { get; set; }
    /// <summary>
    /// 名稱
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 星級
    /// </summary>
    public int LevelStart { get; set; }
}

其中每個「角色」擁有多把「武器」,同時與一種「元素」對應

C. 數據展示

可以看出,「琴」與「風」元素對應,「可莉」與「火」元素對應

主要內容

通用的結論:所謂驅動程式,就是將 C# 的寫法翻譯為 MongoDB 的查詢語句寫法(如 EntityFramework 將 C# 寫法翻譯為 Sql 語句)。

1. 更新數組類型欄位的某個元素

不同於關係型資料庫 SQL 的 UPDATE … SET … WHERE …,MongoDB 中更新嵌套的類型可以使用 「outer.inner」 的形式來表示嵌套對象的對應欄位。而更新嵌套的數組元素則要繞一個彎子。比如我們要把 「琴」 的 「手裏劍」 改為 「新手大刀」,可以這樣寫:

var update = Builders<YuanshenRole>.Update
                            .Set(w => w.Weapons[-1].Name, "新手大刀");
collectionRole.UpdateOne(role => role.Name == "琴" && role.Weapons.Any(w => w.Name == "手裏劍"), update);

「奇妙」之處在於:在查詢時隱式地查詢數組中的內容(Any方法),並且在更新時使用 -1 對應要更新的數組元素

對比下原生的寫法:

db.YuanshenRole.updateMany({Name: "琴", "Weapons.Name":"手裏劍"}, {$set:{"Weapons.$.Name":"新手大刀"}})

注意此處的 $ 符號。驅動正是將 -1 替換為了 $ 符號,參見源程式碼

2. 關聯查詢

對於關係型資料庫而言,為了符合資料庫的設計三大範式,不可避免的要建立多張表,關聯也是家常便飯, MongoDB 中的關聯思路也基本一致。

此時需要查詢:角色和其對應的元素,則需要關聯 角色表元素表

Linq 方式關聯查詢的程式碼為:(注意:要引入 MongoDB.Driver)

var query = from role in collectionRole.AsQueryable().Where(a => a.Weapons.Count > 0)
            join element in collectionElement.AsQueryable()
                on role.ElementId equals element._id
            select new
            {
                role.Name,
                element.ElementName
            };
var dataList = query.ToList();
foreach(var data in dataList)
{
    Console.WriteLine(string.Format("角色名:{0},元素屬性:{1}", data.Name, data.ElementName));
}

查詢結果:

角色名:琴,元素屬性:風
角色名:可莉,元素屬性:火

關聯欄位的類型要一致。這種方式可以正常查出結果,可如果稍微變化一下,如:

A.(報錯)給關聯的子表加入 where 條件

查閱了許多資料,似乎並沒有說 MongoDB 關聯的子表不能加入條件,或者說 MongoDB 關聯的子表可以加入條件 ,最後只有一篇泛泛的描述

B.(報錯)查詢時查出整個「文檔」

個人認為這是驅動「翻譯」的問題

3. 關聯查詢的類原生寫法

先看下 Mongo Shell 中的寫法:

C# 語言中類原生的寫法為:

BsonDocument query = new BsonDocument
    {
        { "Weapons", new BsonDocument{ { "$size", 3 } } }
    };

// 匹配條件
PipelineStageDefinition<BsonDocument, BsonDocument> match =
    PipelineStageDefinitionBuilder.Match<BsonDocument>(query);

// 關聯條件
PipelineStageDefinition<BsonDocument, BsonDocument> lookup =
    PipelineStageDefinitionBuilder.Lookup<BsonDocument, BsonDocument, BsonDocument>(
        dbYuanShen.GetCollection<BsonDocument>("Element"),  # 要關聯的表
        (FieldDefinition<BsonDocument>)"ElementId",  # 主表的欄位
        (FieldDefinition<BsonDocument>)"_id",  # 子表的對應欄位
        (FieldDefinition<BsonDocument>)"ElementAttr"); # 此處將關聯後得到的子表對應欄位設置為 「ElementAttr」

// 組合併形成最終查詢
var pipeline = PipelineDefinition<BsonDocument, BsonDocument>.Create(
					new List<PipelineStageDefinition<BsonDocument, BsonDocument>> { match, lookup });

// 執行並得到數據
var dataList = dbYuanShen.GetCollection<BsonDocument>("YuanshenRole").Aggregate(pipeline).ToList();

var resultList = dataList.Select(a => new
{
    Name = a.GetValue("Name").AsString,
    ElementName = a.GetValue("ElementAttr").AsBsonArray[0]  # 與上面設置的子表欄位對應,不過要注意類型為數組
                    .AsBsonDocument.GetValue("ElementName").AsString
}).ToList();
foreach (var result in resultList)
{
    Console.WriteLine(string.Format("角色名:{0},元素屬性:{1}", result.Name, result.ElementName));
}

可以看出,在形成查詢的 Json 時還是有一點複雜的,至少泛型的名稱就夠長的了QAQ。不過邏輯還算清晰,對比原生的寫法也算是勉勉強強可以接受吧。需要注意的是:

(1)BsonDocument 的創建中,new BsonDocument{ { A, B } } 等價於 new BsonDocument{ new BsonElement(A, B) }

(2)如果想要在查詢中使用 null, 應使用 BsonValue.Null

(3)時間的時區問題,應使用 BsonUtils.ToUniversalTime(MY_TIME) 轉換一下時間。而在上面的 Linq 寫法中,如果屬性(如 CreateTime)添加了 BsonDateTimeOptions 特性 ,查詢時驅動程式會根據類的定義自動轉換,但也應注意作業系統的時區設置。

(4)如果要使用 $in 和 BsonArray,可以使用 BsonArray.Create(IEnumerable類型的值) 創建 BsonArray 對象

(5)此處的查詢實際上是左外連接,從表的映射欄位(即程式碼中的 ElementAttr)所表示的類型實際上是一個數組,如果子表中沒有對應的關聯,則映射欄位結果為空數組。此處由於確定有關聯數據,因此直接使用即可。如果要展開,可以在 pipeline 中加入 $unwind 操作。

4. 變動時多欄位問題

A. 情況1,定義的類裡面新加了欄位 而 MongoD資料庫沒加

不影響。新加的欄位會默認賦值為其類型的默認值

B. 情況2,MongoDB資料庫中的新加了欄位 而 定義的類中沒加

會報錯。如下:

但可通過在類上添加特性 BsonIgnoreExtraElements 來忽略資料庫中多出的欄位

番外篇

眾所周知,MongoDB 中查詢時如果要匹配數組的元素數量,只能寫等於,而不能寫大於或者小於。而我在 「2.關聯查詢」中居然寫了 Count > 0,而且並沒有報錯,這是怎麼回事呢。為了泛化這種情況,我把 Count > 0 改成 Count > 1,然後調試下:

原來是 指定了某個數組元素 配合 $exists 判斷的,妙啊!

參考

MongoDB .Net Driver(C#驅動) – 內嵌數組/嵌入文檔的操作(增加、刪除、修改、查詢(Linq 分頁))(其中還有顯式地調用擴展方法的講解)
//blog.csdn.net/qq_27441069/article/details/79586372

Mongodb在CSharp里實現Aggregate
//www.cnblogs.com/lori/p/6864134.html

Tags: