MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Aggrega $ ricerca con C#

Non è necessario analizzare il JSON. Tutto qui può effettivamente essere fatto direttamente con LINQ o le interfacce Aggregate Fluent.

Sto solo usando alcune lezioni dimostrative perché la domanda non dà molto su cui andare avanti.

Configurazione

Fondamentalmente abbiamo due raccolte qui, essendo

entità

{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }

e altri

{
        "_id" : ObjectId("5b08cef10a8a7614c70a5712"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
        "name" : "Sub-A"
}
{
        "_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
        "name" : "Sub-B"
}

E un paio di classi a cui associarli, proprio come esempi molto semplici:

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

Query

Interfaccia fluida

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

Richiesta inviata al server:

[
  { "$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 } }
]

Probabilmente il più facile da capire poiché l'interfaccia fluente è fondamentalmente la stessa della struttura BSON generale. La $lookup stage ha tutti gli stessi argomenti e $arrayElemAt è rappresentato con First() . Per il $sort puoi semplicemente fornire un documento BSON o un'altra espressione valida.

Un'alternativa è la nuova forma espressiva di $lookup con un'istruzione sub-pipeline per MongoDB 3.6 e versioni successive.

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

Richiesta inviata al server:

[ 
  { "$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 } }
]

Il "Builder" fluente non supporta ancora direttamente la sintassi, né le espressioni LINQ supportano il $expr operatore, tuttavia puoi ancora costruire usando BsonDocument e BsonArray o altre espressioni valide. Qui "digitiamo" anche il $unwind risultato per applicare un $sort utilizzando un'espressione anziché un BsonDocument come mostrato in precedenza.

A parte altri usi, un compito principale di una "sotto-pipeline" è ridurre i documenti restituiti nell'array di destinazione di $lookup . Anche il $unwind qui serve allo scopo di essere effettivamente "unito" nel $lookup istruzione sull'esecuzione del server, quindi questo è in genere più efficiente del semplice afferrare il primo elemento dell'array risultante.

Partecipa al gruppo interrogabile

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

Richiesta inviata al server:

[ 
  { "$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 } }
]

Questo è quasi identico ma usa solo l'interfaccia diversa e produce un'istruzione BSON leggermente diversa, e in realtà solo a causa della denominazione semplificata nelle istruzioni funzionali. Questo fa apparire l'altra possibilità di usare semplicemente un $unwind come prodotto da 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);

Richiesta inviata al server:

[
  { "$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 posizionando un $unwind seguendo direttamente $lookup è in realtà un "modello ottimizzato" per il framework di aggregazione. Tuttavia il driver .NET rovina tutto in questa combinazione forzando un $project in mezzo invece di usare la denominazione implicita su "as" . In caso contrario, questo è effettivamente meglio di $arrayElemAt quando sai di avere "uno" risultato correlato. Se vuoi il $unwind "coalescenza", allora è meglio usare l'interfaccia fluente, o una forma diversa come dimostrato più avanti.

Querable Naturale

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;

Richiesta inviata al server:

[
  { "$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 } }
]

Tutto abbastanza familiare e davvero solo fino alla denominazione funzionale. Proprio come con l'utilizzo di $unwind opzione:

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;

Richiesta inviata al server:

[ 
  { "$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 } }
]

Che in realtà sta usando il modulo "coalescenza ottimizzata". Il traduttore insiste ancora per aggiungere un $project poiché abbiamo bisogno dell'intermedio select per rendere valida la dichiarazione.

Riepilogo

Quindi ci sono molti modi per arrivare essenzialmente a quella che è fondamentalmente la stessa istruzione di query con esattamente gli stessi risultati. Mentre "potresti" analizzare il JSON in BsonDocument form e invialo al fluente Aggregate() comando, in genere è meglio utilizzare i builder naturali o le interfacce LINQ poiché si associano facilmente alla stessa istruzione.

Le opzioni con $unwind sono in gran parte mostrati perché anche con una corrispondenza "singolare" quella forma di "coalescenza" è in realtà molto più ottimale rispetto all'utilizzo di $arrayElemAt per prendere il "primo" elemento dell'array. Questo diventa ancora più importante con considerazioni su cose come il limite BSON dove il $lookup l'array di destinazione potrebbe far sì che il documento padre superi i 16 MB senza ulteriori filtri. C'è un altro post qui su Aggregate $lookup La dimensione totale dei documenti nella pipeline corrispondente supera la dimensione massima del documento in cui discuto effettivamente di come evitare che tale limite venga raggiunto usando tali opzioni o altri Lookup() sintassi disponibile solo per l'interfaccia fluente in questo momento.