【问题标题】:Aggregate $lookup with C#使用 C# 聚合 $lookup
【发布时间】:2018-05-25 13:29:47
【问题描述】:

我有以下 MongoDb 查询工作:

db.Entity.aggregate(
    [
        {
            "$match":{"Id": "12345"}
        },
        {
            "$lookup": {
                "from": "OtherCollection",
                "localField": "otherCollectionId",
                "foreignField": "Id",
                "as": "ent"
            }
        },
        { 
            "$project": { 
                "Name": 1,
                "Date": 1,
                "OtherObject": { "$arrayElemAt": [ "$ent", 0 ] } 
            }
        },
        { 
            "$sort": { 
                "OtherObject.Profile.Name": 1
            } 
        }
    ]
)

这会检索与来自另一个集合的匹配对象连接的对象列表。

有人知道我如何在 C# 中使用 LINQ 或使用这个确切的字符串吗?

我尝试使用以下代码,但似乎找不到 QueryDocumentMongoCursor 的类型 - 我认为它们已被弃用?

BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);

【问题讨论】:

    标签: c# mongodb aggregation-framework mongodb-.net-driver


    【解决方案1】:

    无需解析 JSON。这里的一切实际上都可以直接使用 LINQ 或 Aggregate Fluent 接口完成。

    只是使用一些演示课程,因为这个问题并没有太多的继续。

    设置

    基本上我们这里有两个集合,分别是

    实体

    { "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
    { "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
    

    其他人

    {
            "_id" : ObjectId("5b08cef10a8a7614c70a5712"),
            "entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
            "name" : "Sub-A"
    }
    {
            "_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
            "entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
            "name" : "Sub-B"
    }
    

    以及将它们绑定到的几个类,就像非常基本的示例一样:

    public class Entity
    {
      public ObjectId id;
      public string name { get; set; }
    }
    
    public class Other
    {
      public ObjectId id;
      public ObjectId entity { get; set; }
      public string name { get; set; }
    }
    
    public class EntityWithOthers
    {
      public ObjectId id;
      public string name { get; set; }
      public IEnumerable<Other> others;
    }
    
     public class EntityWithOther
    {
      public ObjectId id;
      public string name { get; set; }
      public Other others;
    }
    

    查询

    流畅的界面

    var listNames = new[] { "A", "B" };
    
    var query = entities.Aggregate()
        .Match(p => listNames.Contains(p.name))
        .Lookup(
          foreignCollection: others,
          localField: e => e.id,
          foreignField: f => f.entity,
          @as: (EntityWithOthers eo) => eo.others
        )
        .Project(p => new { p.id, p.name, other = p.others.First() } )
        .Sort(new BsonDocument("other.name",-1))
        .ToList();
    

    请求发送到服务器:

    [
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : { 
        "from" : "others",
        "localField" : "_id",
        "foreignField" : "entity",
        "as" : "others"
      } }, 
      { "$project" : { 
        "id" : "$_id",
        "name" : "$name",
        "other" : { "$arrayElemAt" : [ "$others", 0 ] },
        "_id" : 0
      } },
      { "$sort" : { "other.name" : -1 } }
    ]
    

    可能是最容易理解的,因为流畅的界面与一般的 BSON 结构基本相同。 $lookup 阶段具有所有相同的参数,$arrayElemAtFirst() 表示。对于 $sort,您可以简单地提供 BSON 文档或其他有效表达式。

    另一种是 $lookup 的新表达形式,带有用于 MongoDB 3.6 及更高版本的子管道语句。

    BsonArray subpipeline = new BsonArray();
    
    subpipeline.Add(
      new BsonDocument("$match",new BsonDocument(
        "$expr", new BsonDocument(
          "$eq", new BsonArray { "$$entity", "$entity" }  
        )
      ))
    );
    
    var lookup = new BsonDocument("$lookup",
      new BsonDocument("from", "others")
        .Add("let", new BsonDocument("entity", "$_id"))
        .Add("pipeline", subpipeline)
        .Add("as","others")
    );
    
    var query = entities.Aggregate()
      .Match(p => listNames.Contains(p.name))
      .AppendStage<EntityWithOthers>(lookup)
      .Unwind<EntityWithOthers, EntityWithOther>(p => p.others)
      .SortByDescending(p => p.others.name)
      .ToList();
    

    请求发送到服务器:

    [ 
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : {
        "from" : "others",
        "let" : { "entity" : "$_id" },
        "pipeline" : [
          { "$match" : { "$expr" : { "$eq" : [ "$$entity", "$entity" ] } } }
        ],
        "as" : "others"
      } },
      { "$unwind" : "$others" },
      { "$sort" : { "others.name" : -1 } }
    ]
    

    Fluent "Builder" 还不直接支持语法,LINQ 表达式也不支持 $expr 运算符,但是您仍然可以使用 BsonDocumentBsonArray 或其他有效表达式进行构造。在这里,我们还“键入”$unwind 结果,以便使用表达式而不是前面所示的BsonDocument 应用$sort

    除其他用途外,“子管道”的主要任务是减少在$lookup 的目标数组中返回的文档。此外,这里的$unwind 的目的实际上是将being "merged" 放入服务器执行时的$lookup 语句中,因此这通常比仅获取结果数组的第一个元素更有效。

    可查询的 GroupJoin

    var query = entities.AsQueryable()
        .Where(p => listNames.Contains(p.name))
        .GroupJoin(
          others.AsQueryable(),
          p => p.id,
          o => o.entity,
          (p, o) => new { p.id, p.name, other = o.First() }
        )
        .OrderByDescending(p => p.other.name);
    

    请求发送到服务器:

    [ 
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : {
        "from" : "others",
        "localField" : "_id",
        "foreignField" : "entity",
        "as" : "o"
      } },
      { "$project" : {
        "id" : "$_id",
        "name" : "$name",
        "other" : { "$arrayElemAt" : [ "$o", 0 ] },
        "_id" : 0
      } },
      { "$sort" : { "other.name" : -1 } }
    ]
    

    这几乎是相同的,只是使用了不同的接口并产生了稍微不同的 BSON 语句,实际上只是因为函数语句中的简化命名。这确实带来了另一种可能性,即简单地使用由SelectMany() 生成的$unwind

    var query = entities.AsQueryable()
      .Where(p => listNames.Contains(p.name))
      .GroupJoin(
        others.AsQueryable(),
        p => p.id,
        o => o.entity,
        (p, o) => new { p.id, p.name, other = o }
      )
      .SelectMany(p => p.other, (p, other) => new { p.id, p.name, other })
      .OrderByDescending(p => p.other.name);
    

    请求发送到服务器:

    [
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : {
        "from" : "others",
        "localField" : "_id",
        "foreignField" : "entity",
        "as" : "o"
      }},
      { "$project" : {
        "id" : "$_id",
        "name" : "$name",
        "other" : "$o",
        "_id" : 0
      } },
      { "$unwind" : "$other" },
      { "$project" : {
        "id" : "$id",
        "name" : "$name",
        "other" : "$other",
        "_id" : 0
      }},
      { "$sort" : { "other.name" : -1 } }
    ]
    

    通常在$lookup 之后直接放置$unwind 实际上是聚合框架的"optimized pattern"。然而,.NET 驱动程序确实在这种组合中搞砸了,通过在两者之间强制使用 $project 而不是使用 "as" 上的隐含命名。如果不是这样,当您知道您有“一个”相关结果时,这实际上比$arrayElemAt 更好。如果你想要$unwind "coalescence",那么你最好使用流畅的界面,或者稍后演示的不同形式。

    可恶自然

    var query = from p in entities.AsQueryable()
                where listNames.Contains(p.name) 
                join o in others.AsQueryable() on p.id equals o.entity into joined
                select new { p.id, p.name, other = joined.First() }
                into p
                orderby p.other.name descending
                select p;
    

    请求发送到服务器:

    [
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : {
        "from" : "others",
        "localField" : "_id",
        "foreignField" : "entity",
        "as" : "joined"
      } },
      { "$project" : {
        "id" : "$_id",
        "name" : "$name",
        "other" : { "$arrayElemAt" : [ "$joined", 0 ] },
        "_id" : 0
      } },
      { "$sort" : { "other.name" : -1 } }
    ]
    

    一切都非常熟悉,实际上只是功能命名。就像使用 $unwind 选项一样:

    var query = from p in entities.AsQueryable()
                where listNames.Contains(p.name) 
                join o in others.AsQueryable() on p.id equals o.entity into joined
                from sub_o in joined.DefaultIfEmpty()
                select new { p.id, p.name, other = sub_o }
                into p
                orderby p.other.name descending
                select p;
    

    请求发送到服务器:

    [ 
      { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
      { "$lookup" : {
        "from" : "others",
        "localField" : "_id",
        "foreignField" : "entity",
        "as" : "joined"
      } },
      { "$unwind" : { 
        "path" : "$joined", "preserveNullAndEmptyArrays" : true
      } }, 
      { "$project" : { 
        "id" : "$_id",
        "name" : "$name",
        "other" : "$joined",
        "_id" : 0
      } }, 
      { "$sort" : { "other.name" : -1 } }
    ]
    

    实际上是使用"optimized coalescence" 表单。翻译者仍然坚持添加$project,因为我们需要中间的select 来使语句有效。

    总结

    因此,有很多方法可以基本上得出具有完全相同结果的基本相同查询语句。虽然您“可以”将 JSON 解析为 BsonDocument 表单并将其提供给流畅的 Aggregate() 命令,但通常最好使用自然生成器或 LINQ 接口,因为它们可以轻松映射到同一语句。

    $unwind 的选项大部分都显示出来了,因为即使使用“单数”匹配,“合并”形式实际上比使用 $arrayElemAt 获取“第一个”数组元素要好得多。考虑到诸如 BSON 限制之类的事情,这一点甚至变得更加重要,其中 $lookup 目标数组可能导致父文档超过 16MB 而无需进一步过滤。 Aggregate $lookup Total size of documents in matching pipeline exceeds maximum document size 上还有另一篇文章,我实际上讨论了如何通过使用此类选项或其他 Lookup() 语法来避免达到限制,目前仅适用于流利界面。

    【讨论】:

    • 非常好的答案 - 感谢您抽出宝贵时间。
    • 我还要感谢你,这让我向前迈出了一步。不幸的是,自从这篇文章以来,驱动程序似乎发生了变化。我没有看到不带类型参数的 Aggregate 方法。另外请您解释一下“实体”是什么?
    • 哇!完美的答案。谢谢!
    • 我在这部分使用@ as: (EntityWithOthers eo) => eo.others like @ as : "someName",但它给出了错误,你能解释一下吗?
    猜你喜欢
    • 2016-10-14
    • 1970-01-01
    • 2022-01-05
    • 2021-06-04
    • 2023-03-09
    • 1970-01-01
    • 2020-06-21
    • 2017-05-12
    • 1970-01-01
    相关资源
    最近更新 更多