使用C#进行Aggregate $lookup

24

我已经成功使用以下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);
1个回答

85

无需解析JSON。实际上,这里的所有操作都可以直接使用LINQ或Aggregate Fluent接口完成。

只是因为问题并没有提供太多信息,所以使用了一些演示类。

设置

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

entities

{ "_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文档或其他有效的表达式。

另一种选择是MongoDB 3.6及以上版本的新表达形式,使用子管道语句的$lookup

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 还起到将其 被“合并” 到服务器执行的 $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实际上是聚合框架的一种"优化模式"。然而,.NET驱动程序在这种组合中会通过在其中强制使用$project而破坏这个优化模式,而不是使用"as"上的隐式命名。如果不是因为这个问题,当您知道您有“一个”相关结果时,这比$arrayElemAt实际上更好。如果您想要$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
            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 } }
]

实际上使用的是优化凝聚形式。翻译器仍然坚持添加$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()语法来避免达到该限制。

6
非常棒的回答 - 感谢您抽出时间来做这件事。 - TomSelleck
我也想感谢你,这让我向前迈了一步。不幸的是,自从这篇文章发布以来,驱动程序似乎已经改变了。我没有看到一个不带类型参数的Aggregate方法。另外,您能否详细说明一下“entities”是什么? - Kevin Burton
哇!完美的答案。谢谢! - Rejwanul Reja
我在这部分中使用@作为:(EntityWithOthers eo) => eo.others like @ as : "someName",但它会出现错误,有人能解释一下吗? - Hridoy_089
1
如果我们需要在“others”集合上进行过滤,该怎么办? - Seiko Santana

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接