无需解析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
阶段具有所有相同的参数,并且$arrayElemAt
用First()
表示。对于$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
操作符,但是您仍然可以使用 BsonDocument
和 BsonArray
或其他有效的表达式进行构建。在此,我们还通过“类型化” $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()
语法来避免达到该限制。