使用 C# 聚合 $lookup

Aggregate $lookup with C#

我有以下 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);

不需要解析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 stage has all the same arguments and the $arrayElemAt is represented with First(). For the $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 operator, however you can still construct using BsonDocument and BsonArray or other valid expressions. Here we also "type" the $unwind result in order to apply a $sort 使用表达式而不是前面显示的 BsonDocument

除了其他用途,"sub-pipeline" 的一个主要任务是减少 $lookup 的目标数组中返回的文档。此外,此处的 $unwind 的目的实际上是 在服务器执行时 "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" 上使用隐含命名,从而在这种组合中搞砸了。如果不是这样,当你知道你有 "one" 相关结果时,这实际上比 $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" form. The translator still insists on adding a $project 因为我们需要中间 select 来使语句有效。

总结

因此,有很多方法可以从本质上得出具有完全相同结果的基本相同的查询语句。当您 "could" 将 JSON 解析为 BsonDocument 形式并将其提供给流畅的 Aggregate() 命令时,通常最好使用自然构建器或 LINQ 接口,因为它们很容易做到映射到相同的语句。

带有 $unwind 的选项在很大程度上显示出来,因为即使使用 "singular" 匹配,"coalescence" 形式实际上比使用 $arrayElemAt 更优化 取 "first" 数组元素。考虑到像 BSON 限制这样的事情,这甚至变得更加重要,其中 $lookup 目标数组可能导致父文档在没有进一步过滤的情况下超过 16MB。在 这里还有另一个 post,我实际上讨论了如何通过使用此类选项或其他 Lookup() 语法目前仅适用于 Fluent 界面。