MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Agréger $lookup avec C#

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.