zoukankan      html  css  js  c++  java
  • 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 分页))(其中还有显式地调用扩展方法的讲解)
    https://blog.csdn.net/qq_27441069/article/details/79586372

    Mongodb在CSharp里实现Aggregate
    https://www.cnblogs.com/lori/p/6864134.html

  • 相关阅读:
    学习淘宝指数有感
    STL学习小结
    Java里泛型有什么作用
    android 内存泄漏分析技巧
    学道1.3
    严苛模式(StrictMode)
    年龄大了还能够学习编程吗
    ORACLE EXP命令
    数学之路-python计算实战(13)-机器视觉-图像增强
    《C语言编写 学生成绩管理系统》
  • 原文地址:https://www.cnblogs.com/battor/p/c_sharp_mongodb_driver_update_array_and_join_traps.html
Copyright © 2011-2022 走看看