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

$lookup mehrere Ebenen ohne $unwind?

Abhängig von Ihrer verfügbaren MongoDB-Version gibt es natürlich ein paar Ansätze. Diese variieren je nach Verwendung von $lookup bis hin zur Aktivierung der Objektmanipulation auf .populate() Ergebnis über .lean() .

Ich bitte Sie, die Abschnitte sorgfältig zu lesen und sich bewusst zu sein, dass möglicherweise nicht alles so ist, wie es scheint, wenn Sie Ihre Implementierungslösung in Betracht ziehen.

MongoDB 3.6, "verschachtelte" $lookup

Mit MongoDB 3.6 ist die $lookup Der Operator erhält die zusätzliche Möglichkeit, eine pipeline einzufügen statt einfach einen „lokalen“ mit einem „fremden“ Schlüsselwert zu verbinden, bedeutet dies, dass Sie im Wesentlichen jeden $lookup durchführen können als "verschachtelt" innerhalb dieser Pipeline-Ausdrücke

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])

Dies kann wirklich sehr leistungsfähig sein, wie Sie aus der Perspektive der ursprünglichen Pipeline sehen, es kennt wirklich nur das Hinzufügen von Inhalten zu den "reviews" Array und dann sieht jeder nachfolgende "verschachtelte" Pipeline-Ausdruck auch immer nur seine "inneren" Elemente aus dem Join.

Es ist leistungsfähig und in gewisser Hinsicht vielleicht etwas klarer, da alle Feldpfade relativ zur Verschachtelungsebene sind, aber es beginnt mit dem Kriechen der Einrückung in der BSON-Struktur, und Sie müssen sich darüber im Klaren sein, ob Sie mit Arrays übereinstimmen oder singuläre Werte beim Durchlaufen der Struktur.

Beachten Sie, dass wir hier auch Dinge tun können, wie z. B. "die Autoreneigenschaft glätten", wie in den "comments" zu sehen ist Array-Einträge. Alle $lookup Die Zielausgabe kann ein "Array" sein, aber innerhalb einer "Sub-Pipeline" können wir dieses Einzelelement-Array in nur einen einzigen Wert umformen.

Standard-MongoDB-$lookup

Den "Beitritt auf dem Server" beibehalten, Sie können dies tatsächlich mit $lookup tun , aber es braucht nur eine Zwischenverarbeitung. Dies ist der seit langem bestehende Ansatz, ein Array mit $unwind zu dekonstruieren und die Verwendung von $group Stufen zum Neuaufbau von Arrays:

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])

Dies ist wirklich nicht so abschreckend, wie Sie vielleicht zunächst denken, und folgt einem einfachen Muster von $lookup und $unwind während Sie durch jedes Array fortschreiten.

Der "author" Detail ist natürlich singulär, also wollen Sie es einfach so lassen, sobald es "abgewickelt" ist, das Feld hinzufügen und den Prozess des "Zurückrollens" in die Arrays starten.

Es gibt nur zwei Ebenen, um den ursprünglichen Venue wiederherzustellen Dokument, also ist die erste Detailebene von Review um die "comments" neu zu erstellen Reihe. Alles, was Sie tun müssen, ist $push der Pfad von "$reviews.comments" um diese zu sammeln, und solange der "$reviews._id" Das Feld befindet sich in der "Gruppierungs-ID". Die einzigen anderen Dinge, die Sie behalten müssen, sind alle anderen Felder. Sie können all dies in die _id einfügen ebenso, oder Sie können $first verwenden .

Damit gibt es nur noch eine $group Bühne, um zum Venue zurückzukehren selbst. Diesmal ist der Gruppierungsschlüssel "$_id" natürlich mit allen Eigenschaften des Veranstaltungsortes selbst mit $first und die restlichen "$review" Details gehen zurück in ein Array mit $push . Natürlich die "$comments" Ausgabe der vorherigen $group wird zu "review.comments" Pfad.

Das Arbeiten an einem einzelnen Dokument und seinen Beziehungen ist nicht wirklich schlimm. Das $unwind Pipeline-Betreiber können allgemein ein Leistungsproblem sein, aber im Zusammenhang mit dieser Verwendung sollte es keine so großen Auswirkungen haben.

Da die Daten immer noch "auf dem Server zusammengeführt" werden, gibt es noch viel weniger Verkehr als die andere verbleibende Alternative.

JavaScript-Manipulation

Der andere Fall hier ist natürlich, dass Sie, anstatt Daten auf dem Server selbst zu ändern, tatsächlich das Ergebnis manipulieren. In den meisten In einigen Fällen würde ich diesen Ansatz befürworten, da alle "Hinzufügungen" zu den Daten wahrscheinlich am besten auf dem Client behandelt werden.

Das Problem natürlich bei der Verwendung von populate() ist das, während es 'aussehen' kann ein viel einfacherer Prozess, es ist tatsächlich KEIN JOIN in irgendeiner Weise. Alle populate() tatsächlich tut, ist "verstecken" der zugrunde liegende Vorgang des Einreichens von mehreren Abfragen an die Datenbank und dann Warten auf die Ergebnisse durch asynchrone Behandlung.

Also das "Aussehen" eines Joins ist eigentlich das Ergebnis mehrerer Anfragen an den Server und der anschließenden „clientseitigen Manipulation“ der Daten, um die Details in Arrays einzubetten.

Abgesehen von dieser klaren Warnung dass die Leistungsmerkmale bei weitem nicht mit denen eines Servers $lookup vergleichbar sind , der andere Vorbehalt ist natürlich, dass die "Mongoose-Dokumente" im Ergebnis keine einfachen JavaScript-Objekte sind, die einer weiteren Manipulation unterliegen.

Um diesen Ansatz zu verwenden, müssen Sie also .lean() hinzufügen -Methode an die Abfrage vor der Ausführung an, um Mungo anzuweisen, "einfache JavaScript-Objekte" anstelle von Document zurückzugeben Typen, die mit an das Modell angehängten Schemamethoden umgewandelt werden. Beachten Sie natürlich, dass die resultierenden Daten keinen Zugriff mehr auf "Instanzmethoden" haben, die sonst mit den verwandten Modellen selbst verknüpft wären:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Jetzt Venue ein einfaches Objekt ist, können wir einfach nach Bedarf bearbeiten und anpassen:

venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);

Es geht also wirklich nur darum, durch jedes der inneren Arrays nach unten zu gehen, bis zu der Ebene, auf der Sie die followers sehen können -Array innerhalb des author Einzelheiten. Der Vergleich kann dann gegen die ObjectId erfolgen Werte, die in diesem Array gespeichert sind, nachdem Sie zuerst .map() verwendet haben um die "String"-Werte zum Vergleich mit der req.user.id zurückzugeben das ist auch ein String (wenn nicht, dann fügen Sie auch .toString() hinzu darauf ), da es im Allgemeinen einfacher ist, diese Werte auf diese Weise über JavaScript-Code zu vergleichen.

Ich muss jedoch noch einmal betonen, dass es "einfach aussieht", aber es ist tatsächlich die Art von Dingen, die Sie für die Systemleistung wirklich vermeiden möchten, da diese zusätzlichen Abfragen und die Übertragung zwischen dem Server und dem Client viel Verarbeitungszeit kosten und selbst aufgrund des Anforderungs-Overheads summiert sich dies zu echten Kosten beim Transport zwischen Hosting-Providern.

Zusammenfassung

Das sind im Grunde Ihre Ansätze, die Sie verfolgen können, abgesehen von "Ihren eigenen rollen", wo Sie tatsächlich die "mehreren Abfragen" durchführen selbst zur Datenbank hinzufügen, anstatt das Hilfsprogramm .populate() zu verwenden ist.

Unter Verwendung der bestückten Ausgabe können Sie die Daten im Ergebnis einfach wie jede andere Datenstruktur bearbeiten, solange Sie .lean() anwenden an die Abfrage, um die einfachen Objektdaten aus den zurückgegebenen Mongoose-Dokumenten zu konvertieren oder anderweitig zu extrahieren.

Während die aggregierten Ansätze viel komplizierter aussehen, gibt es "viele" weitere Vorteile, diese Arbeit auf dem Server zu erledigen. Größere Ergebnismengen können sortiert werden, Berechnungen können zur weiteren Filterung durchgeführt werden und natürlich erhalten Sie eine "Einzelantwort" zu einer "einzelnen Anfrage" auf den Server übertragen werden, alles ohne zusätzlichen Overhead.

Es ist durchaus vertretbar, dass die Pipelines selbst einfach auf der Grundlage von Attributen konstruiert werden könnten, die bereits im Schema gespeichert sind. Es sollte also nicht allzu schwierig sein, Ihre eigene Methode zu schreiben, um diese "Konstruktion" basierend auf dem beigefügten Schema durchzuführen.

Längerfristig natürlich $lookup ist die bessere Lösung, aber Sie müssen wahrscheinlich etwas mehr Arbeit in die anfängliche Codierung stecken, wenn Sie natürlich nicht einfach von dem kopieren, was hier aufgelistet ist;)