web-dev-qa-db-fra.com

Recherche agrégée $ avec C #

J'ai la requête MongoDb suivante qui fonctionne:

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
            } 
        }
    ]
)

Cela récupère une liste d'objets joints à un objet correspondant d'une autre collection.

Est-ce que quelqu'un sait comment je peux l'utiliser en C # en utilisant LINQ ou en utilisant cette chaîne exacte?

J'ai essayé d'utiliser le code suivant, mais il ne semble pas trouver les types pour QueryDocument et MongoCursor - Je pense qu'ils ont été dépréciés?

BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);
12
TomSelleck

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.

Il suffit d'utiliser des cours de démonstration, car la question ne donne pas vraiment grand-chose.

Installer

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, 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();

Demande 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 la plus facile à comprendre car l'interface fluide est fondamentalement la même que la structure BSON générale. L'étape $lookup 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();

Demande 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 directement en charge la syntaxe, pas plus que les expressions LINQ ne prennent en charge l'opérateur $expr , mais vous pouvez toujours construire en utilisant BsonDocument et BsonArray ou d'autres expressions valides. Ici, nous "tapons" également le résultat $unwind afin d'appliquer un $sort en utilisant une expression plutôt qu'un BsonDocument comme indiqué précédemment.

Mis à part d'autres utilisations, une tâche principale d'un "sous-pipeline" consiste à réduire les documents renvoyés dans le tableau cible de $lookup . De même, le $unwind sert ici à [être "fusionné" dans l'instruction $lookup sur le serveur l'exécution, c'est donc généralement plus efficace que de simplement saisir le premier élément du tableau résultant.

Groupe 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);

Demande 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 } }
]

C'est presque identique mais en utilisant simplement l'interface différente et produit une instruction BSON légèrement différente, et vraiment uniquement en raison de la dénomination simplifiée dans les instructions fonctionnelles. Cela fait apparaître l'autre possibilité d'utiliser simplement un $unwind tel que produit à partir d'une 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);

Demande 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 } }
]

Normalement, placer un $unwind directement après $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 plutôt qu'en utilisant la dénomination implicite sur le "as". Sinon, c'est en fait mieux que le $arrayElemAt quand vous savez que vous avez "un" résultat lié. Si vous voulez la $unwind "coalescence", alors vous feriez mieux d'utiliser l'interface fluide, ou une forme différente comme démontré plus loin.

Querable 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;

Demande 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 } }
]

Tout cela est assez familier et vraiment juste pour le nommage fonctionnel. Tout comme avec l'option $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;

Demande 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 sur l'ajout d'un $project car nous avons besoin de l'intermédiaire select pour rendre la déclaration valide.

Sommaire

Il existe donc plusieurs façons d'arriver à ce qui est essentiellement la même instruction de requête avec exactement les mêmes résultats. Alors que vous "pourriez" analyser le JSON en BsonDocument et le nourrir avec la commande fluent Aggregate(), il est généralement préférable d'utiliser les constructeurs naturels ou les interfaces LINQ car ils mappent facilement sur le même déclaration.

Les options avec $unwind sont largement affichées parce que même avec une correspondance "singulière", cette forme de "coalescence" est en réalité beaucoup plus optimale qu'en utilisant $arrayElemAt pour prendre le "premier" élément du tableau. Cela devient même plus important avec des considérations telles que la limite BSON où le tableau cible $lookup pourrait entraîner un dépassement de 16 Mo du document parent sans filtrage supplémentaire. Il y a un autre article ici sur La recherche globale de $ La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document où j'explique comment éviter que cette limite soit atteinte en utilisant de telles options ou d'autres Lookup() syntaxe disponible pour l'interface fluide uniquement pour le moment.

29
Neil Lunn

voici comment le faire avec MongoDB.Entities . dans les cas où deux entités sont dans une relation un-à-plusieurs ou plusieurs-à-plusieurs, vous pouvez obtenir un accès inversé à la relation sans avoir à effectuer les jointures manuellement comme indiqué ci-dessous. [Avertissement: je suis l'auteur de la bibliothèque]

using System;
using System.Linq;
using MongoDB.Entities;
using MongoDB.Driver.Linq;

namespace StackOverflow
{
    public class Program
    {
        public class Author : Entity
        {
            public string Name { get; set; }
            public Many<Book> Books { get; set; }

            public Author() => this.InitOneToMany(() => Books);
        }

        public class Book : Entity
        {
            public string Title { get; set; }
        }

        static void Main(string[] args)
        {
            new DB("test");

            var book = new Book { Title = "The Power Of Now" };
            book.Save();

            var author = new Author { Name = "Eckhart Tolle" };
            author.Save();

            author.Books.Add(book);

            //build a query for finding all books that has Power in the title.
            var bookQuery = DB.Queryable<Book>()
                              .Where(b => b.Title.Contains("Power"));

            //find all the authors of books that has a title with Power in them
            var authors = author.Books
                                .ParentsQueryable<Author>(bookQuery); //also can pass in an ID or array of IDs

            //get the result
            var result = authors.ToArray();

            //output the aggregation pipeline
            Console.WriteLine(authors.ToString());


            Console.ReadKey();
        }
    }
}
0
Ryan Gunner