Il n'est pas nécessaire d'analyser le JSON. Tout ici peut en fait être fait directement avec LINQ ou les interfaces Aggregate Fluent.
J'utilise juste quelques classes de démonstration parce que la question ne donne pas vraiment grand-chose.
Configuration
Fondamentalement, nous avons deux collections ici, étant
entités
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
et autres
{
"_id" : ObjectId("5b08cef10a8a7614c70a5712"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
"name" : "Sub-A"
}
{
"_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
"name" : "Sub-B"
}
Et quelques classes auxquelles les lier, tout comme des exemples très basiques :
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;
}
Requêtes
Interface fluide
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();
Requête envoyée au serveur :
[
{ "$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 } }
]
Probablement le plus facile à comprendre puisque l'interface fluide est fondamentalement la même que la structure générale du BSON. La $lookup
stage a tous les mêmes arguments et le $arrayElemAt
est représenté par First()
. Pour le $sort
vous pouvez simplement fournir un document BSON ou une autre expression valide.
Une alternative est la nouvelle forme expressive de $lookup
avec une instruction de sous-pipeline pour MongoDB 3.6 et supérieur.
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();
Requête envoyée au serveur :
[
{ "$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 } }
]
Le "Builder" Fluent ne prend pas encore en charge la syntaxe directement, et les expressions LINQ ne prennent pas non plus en charge le $expr
, mais vous pouvez toujours construire en utilisant BsonDocument
et BsonArray
ou d'autres expressions valides. Ici, nous "tapons" également le $unwind
résultat afin d'appliquer un $sort
en utilisant une expression plutôt qu'un BsonDocument
comme indiqué précédemment.
Outre d'autres utilisations, une tâche principale d'un "sous-pipeline" est de réduire les documents renvoyés dans le tableau cible de $lookup
. Aussi le $unwind
sert ici à être "fusionné" dans le $lookup
lors de l'exécution du serveur, cela est donc généralement plus efficace que de simplement saisir le premier élément du tableau résultant.
GroupJoin interrogeable
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);
Requête envoyée au serveur :
[
{ "$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 } }
]
Ceci est presque identique mais utilise simplement l'interface différente et produit une instruction BSON légèrement différente, et vraiment uniquement à cause de la dénomination simplifiée dans les instructions fonctionnelles. Cela fait apparaître l'autre possibilité d'utiliser simplement un $unwind
comme produit à partir d'un 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);
Requête envoyée au serveur :
[
{ "$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 } }
]
Placer normalement un $unwind
suivant directement $lookup
est en fait un "modèle optimisé" pour le cadre d'agrégation. Cependant, le pilote .NET gâche cela dans cette combinaison en forçant un $project
entre les deux plutôt que d'utiliser la dénomination implicite sur le "as"
. Sinon pour cela, c'est en fait mieux que le $arrayElemAt
lorsque vous savez que vous avez "un" résultat connexe. Si vous voulez que le $unwind
"coalescence", alors vous feriez mieux d'utiliser l'interface fluide, ou une forme différente comme démontré plus tard.
Naturel querable
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;
Requête envoyée au serveur :
[
{ "$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 } }
]
Tous assez familiers et vraiment juste jusqu'à la dénomination fonctionnelle. Tout comme avec l'utilisation de $unwind
choix :
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;
Requête envoyée au serveur :
[
{ "$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 } }
]
Qui utilise en fait la forme "coalescence optimisée". Le traducteur insiste toujours pour ajouter un $project
puisque nous avons besoin de l'intermédiaire select
afin de valider la déclaration.
Résumé
Il existe donc plusieurs façons d'arriver essentiellement à ce qui est fondamentalement la même instruction de requête avec exactement les mêmes résultats. Alors que vous "pourriez" analyser le JSON en BsonDocument
form et alimentez-le avec le Aggregate()
courant commande, il est généralement préférable d'utiliser les constructeurs naturels ou les interfaces LINQ car ils se mappent facilement sur la même instruction.
Les options avec $unwind
sont largement affichés car même avec une correspondance "singulière", cette forme de "coalescence" est en fait beaucoup plus optimale que d'utiliser $arrayElemAt
pour prendre le "premier" élément du tableau. Cela devient encore plus important avec des considérations telles que la limite BSON où le $lookup
tableau cible pourrait faire en sorte que le document parent dépasse 16 Mo sans autre filtrage. Il y a un autre article ici sur Aggregate $lookup La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document où je discute en fait de la façon d'éviter que cette limite ne soit atteinte en utilisant de telles options ou d'autres Lookup()
syntaxe disponible pour l'interface fluide uniquement pour le moment.