Não há necessidade de analisar o JSON. Tudo aqui pode ser feito diretamente com as interfaces LINQ ou Aggregate Fluent.
Apenas usando algumas aulas de demonstração porque a questão realmente não dá muito para continuar.
Configuração
Basicamente temos aqui duas coleções, sendo
entidades
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
e outros
{
"_id" : ObjectId("5b08cef10a8a7614c70a5712"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
"name" : "Sub-A"
}
{
"_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
"name" : "Sub-B"
}
E algumas classes para vinculá-los, apenas como exemplos muito básicos:
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;
}
Consultas
Interface Fluente
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();
Solicitação enviada ao servidor:
[
{ "$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 } }
]
Provavelmente o mais fácil de entender, pois a interface fluente é basicamente a mesma que a estrutura geral do BSON. A
$lookup
stage tem todos os mesmos argumentos e o $arrayElemAt
é representado com First()
. Para o $sort
você pode simplesmente fornecer um documento BSON ou outra expressão válida. Uma alternativa é a forma expressiva mais recente de
$lookup
com uma instrução de sub-pipeline para MongoDB 3.6 e superior. 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();
Solicitação enviada ao servidor:
[
{ "$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 } }
]
O Fluent "Builder" ainda não suporta a sintaxe diretamente, nem as expressões LINQ suportam o
$expr
operador, no entanto, você ainda pode construir usando BsonDocument
e BsonArray
ou outras expressões válidas. Aqui também "digitamos" o $unwind
resultado para aplicar um $sort
usando uma expressão em vez de um BsonDocument
como mostrado anteriormente. Além de outros usos, uma tarefa principal de um "sub-pipeline" é reduzir os documentos retornados no array de destino de
$lookup
. Também o $unwind
aqui serve ao propósito de realmente ser "mesclado" no $lookup
instrução na execução do servidor, portanto, isso geralmente é mais eficiente do que apenas pegar o primeiro elemento da matriz resultante. Participação de grupo consultável
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);
Solicitação enviada ao servidor:
[
{ "$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 } }
]
Isso é quase idêntico, mas apenas usando a interface diferente e produz uma instrução BSON ligeiramente diferente, e realmente apenas por causa da nomenclatura simplificada nas instruções funcionais. Isso traz a outra possibilidade de simplesmente usar um
$unwind
como produzido a partir de um SelectMany()
: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);
Solicitação enviada ao servidor:
[
{ "$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 } }
]
Normalmente colocando um
$unwind
diretamente seguindo $lookup
é na verdade um "padrão otimizado" para a estrutura de agregação. No entanto, o driver .NET atrapalha isso nessa combinação, forçando um $project
no meio, em vez de usar a nomenclatura implícita no "as"
. Se não fosse por isso, isso é realmente melhor que o $arrayElemAt
quando você sabe que tem "um" resultado relacionado. Se você quiser o $unwind
"coalescência", então é melhor usar a interface fluente ou uma forma diferente, conforme demonstrado posteriormente. Querível Natural
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;
Solicitação enviada ao servidor:
[
{ "$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 } }
]
Tudo bastante familiar e realmente apenas para nomenclatura funcional. Assim como com o uso do
$unwind
opção: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;
Solicitação enviada ao servidor:
[
{ "$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 } }
]
O que na verdade está usando a forma de "coalescência otimizada". O tradutor ainda insiste em adicionar um
$project
já que precisamos do intermediário select
para validar a afirmação. Resumo
Portanto, existem algumas maneiras de chegar essencialmente ao que é basicamente a mesma instrução de consulta com exatamente os mesmos resultados. Enquanto você "poderia" analisar o JSON para
BsonDocument
form e alimente isso para o fluente Aggregate()
comando, geralmente é melhor usar os construtores naturais ou as interfaces LINQ, pois eles mapeiam facilmente na mesma instrução. As opções com
$unwind
são amplamente mostrados porque, mesmo com uma correspondência "singular", a forma de "coalescência" é realmente muito mais ideal do que usar $arrayElemAt
para pegar o "primeiro" elemento do array. Isso se torna ainda mais importante com considerações como o Limite BSON, onde o $lookup
matriz de destino pode fazer com que o documento pai exceda 16 MB sem filtragem adicional. Há outro post aqui em Aggregate $lookup O tamanho total dos documentos no pipeline de correspondência excede o tamanho máximo do documento, onde eu realmente discuto como evitar que esse limite seja atingido usando essas opções ou outros Lookup()
sintaxe disponível para a interface fluente apenas neste momento.