MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

$lookup mit C# aggregieren

JSON muss nicht analysiert werden. Alles hier kann tatsächlich direkt mit LINQ oder den Aggregate Fluent-Schnittstellen erledigt werden.

Verwenden Sie nur einige Demonstrationsklassen, da die Frage nicht wirklich viel zu bieten hat.

Einrichtung

Grundsätzlich haben wir hier zwei Sammlungen, nämlich

Entitäten

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

und andere

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

Und ein paar Klassen, an die sie gebunden werden können, nur als sehr grundlegende Beispiele:

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

Abfragen

Fließende Benutzeroberfläche

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

Anfrage an Server gesendet:

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

Wahrscheinlich am einfachsten zu verstehen, da die fließende Schnittstelle im Grunde die gleiche ist wie die allgemeine BSON-Struktur. Die $lookup stage hat dieselben Argumente und das $arrayElemAt wird mit First() dargestellt . Für $sort Sie können einfach ein BSON-Dokument oder einen anderen gültigen Ausdruck angeben.

Eine Alternative ist die neuere Ausdrucksform von $lookup mit einer Sub-Pipeline-Anweisung für MongoDB 3.6 und höher.

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

Anfrage an Server gesendet:

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

Der Fluent "Builder" unterstützt die Syntax noch nicht direkt, noch unterstützen LINQ-Ausdrücke den $expr -Operator, Sie können jedoch immer noch mit BsonDocument konstruieren und BsonArray oder andere gültige Ausdrücke. Hier "tippen" wir auch den $unwind result, um einen $sort anzuwenden Verwendung eines Ausdrucks statt eines BsonDocument wie zuvor gezeigt.

Abgesehen von anderen Verwendungen besteht eine Hauptaufgabe einer "Sub-Pipeline" darin, die im Zielarray von $lookup zurückgegebenen Dokumente zu reduzieren . Auch das $unwind dient hier dem Zweck, tatsächlich in $lookup "zusammengeführt" zu werden -Anweisung bei der Serverausführung, daher ist dies in der Regel effizienter, als nur das erste Element des resultierenden Arrays abzurufen.

Abfragebarer Gruppenbeitritt

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

Anfrage an Server gesendet:

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

Dies ist fast identisch, verwendet aber nur die andere Schnittstelle und erzeugt eine etwas andere BSON-Anweisung, und zwar wirklich nur wegen der vereinfachten Benennung in den funktionalen Anweisungen. Dies bringt die andere Möglichkeit hervor, einfach ein $unwind zu verwenden wie von einem SelectMany() erzeugt :

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

Anfrage an Server gesendet:

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

Normalerweise wird ein $unwind platziert direkt nach $lookup ist eigentlich ein "optimiertes Muster" für das Aggregationsframework. Der .NET-Treiber bringt dies jedoch in dieser Kombination durcheinander, indem er ein $project erzwingt dazwischen, anstatt die implizite Benennung auf dem "as" zu verwenden . Wenn nicht, ist dies tatsächlich besser als $arrayElemAt wenn Sie wissen, dass Sie "ein" zugehöriges Ergebnis haben. Wenn Sie das $unwind wollen "Koaleszenz", dann sind Sie besser dran, die fließende Schnittstelle zu verwenden, oder eine andere Form, wie später gezeigt wird.

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;

Anfrage an Server gesendet:

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

Alles ziemlich vertraut und wirklich nur auf die funktionale Benennung zurückzuführen. Genau wie bei der Verwendung von $unwind Möglichkeit:

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;

Anfrage an Server gesendet:

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

Was tatsächlich das Formular "optimierte Koaleszenz" verwendet. Der Übersetzer besteht immer noch darauf, ein $project hinzuzufügen da wir das dazwischenliegende select benötigen um die Aussage gültig zu machen.

Zusammenfassung

Es gibt also eine ganze Reihe von Möglichkeiten, im Wesentlichen zu derselben Abfrageanweisung mit genau denselben Ergebnissen zu gelangen. Während Sie den JSON in BsonDocument "parsen" könnten bilden und dies dem Fluent Aggregate() zuführen Befehl, ist es im Allgemeinen besser, die natürlichen Builder oder die LINQ-Schnittstellen zu verwenden, da sie sich leicht auf dieselbe Anweisung abbilden lassen.

Die Optionen mit $unwind werden weitgehend gezeigt, da selbst bei einer "singulären" Übereinstimmung diese "Koaleszenz"-Form tatsächlich weitaus optimaler ist als die Verwendung von $arrayElemAt um das "erste" Array-Element zu nehmen. Dies wird sogar noch wichtiger bei Überlegungen zu Dingen wie dem BSON-Limit, bei dem $lookup Zielarray könnte dazu führen, dass das übergeordnete Dokument ohne weitere Filterung 16 MB überschreitet. Hier gibt es einen weiteren Beitrag zu Aggregate $lookup. Die Gesamtgröße der Dokumente in der übereinstimmenden Pipeline überschreitet die maximale Dokumentgröße, in dem ich tatsächlich erläutere, wie Sie vermeiden können, dass diese Grenze erreicht wird, indem Sie solche Optionen oder andere Lookup() verwenden Syntax, die derzeit nur für die Fluent-Schnittstelle verfügbar ist.