node.js - update - $ mehrere Ebenen suchen, ohne $ abzuwickeln?



pymongo aggregate (1)

Es gibt natürlich ein paar Ansätze, die von Ihrer verfügbaren MongoDB-Version abhängen. Diese variieren von der unterschiedlichen Verwendung von $lookup bis hin zur Aktivierung der Objektmanipulation für das Ergebnis $lookup über $lookup .

Ich fordere Sie auf, die Abschnitte sorgfältig zu lesen und sich darüber im Klaren zu sein, dass möglicherweise nicht alles so ist, wie es scheint, wenn Sie Ihre Implementierungslösung in Betracht ziehen.

MongoDB 3.6, $ lookup "verschachtelt"

Mit MongoDB 3.6 erhält der $lookup Operator die zusätzliche Möglichkeit, einen pipeline Ausdruck einzuschließen, anstatt einfach einen "lokalen" zu "fremden" Schlüsselwert zu verknüpfen. Dies bedeutet, dass Sie im Wesentlichen jeden $lookup als "verschachtelt" innerhalb dieser Pipeline ausführen können 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"
  }},
 ])

Das kann wirklich sehr mächtig sein, wie Sie aus der Perspektive der ursprünglichen Pipeline sehen, es weiß wirklich nur, wie man dem Array "reviews" Inhalt hinzufügt, und dann sieht jeder nachfolgende "verschachtelte" Pipeline-Ausdruck auch immer nur seine "inneren" Elemente aus die Verbindung.

Es ist leistungsfähig und in mancher Hinsicht möglicherweise etwas übersichtlicher, da alle Feldpfade relativ zur Verschachtelungsebene sind. Es führt jedoch dazu, dass Einrückungen in der BSON-Struktur schleichen, und Sie müssen sich darüber im Klaren sein, ob Sie mit Arrays übereinstimmen oder singuläre Werte beim Durchqueren der Struktur.

Beachten Sie, dass wir hier auch Aktionen ausführen können, z. B. das Reduzieren der Author-Eigenschaft, wie in den Array-Einträgen "comments" . Alle $lookup Zielausgaben können ein "Array" sein, aber innerhalb einer "Sub-Pipeline" können wir dieses einzelne Elementarray in nur einen einzigen Wert umformen.

Standard MongoDB $ Lookup

Wenn Sie den "Join auf dem Server" beibehalten, können Sie dies tatsächlich mit $lookup tun, es ist jedoch nur eine Zwischenverarbeitung erforderlich. Dies ist die langjährige Vorgehensweise beim Dekonstruieren eines Arrays mit $unwind und dem Verwenden von $group Stufen zum Neuerstellen 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 dachten, und folgt einem einfachen Muster von $lookup und $unwind während Sie durch die einzelnen Arrays gehen.

Das "author" -Detail ist natürlich singulär. Wenn das "abgewickelt" ist, möchten Sie es einfach so belassen, das Feld hinzufügen und den Vorgang des "Zurückrollens" in die Arrays starten.

Es gibt nur zwei Ebenen, um das ursprüngliche Venue Dokument wiederherzustellen. Die erste Detailebene ist also Review , um das Array "comments" erstellen. Alles, was Sie tun müssen, ist $push den Pfad von "$reviews.comments" zu $push , um diese zu sammeln. Solange sich das Feld "$reviews._id" in der "grouping _id" befindet, müssen Sie nur die anderen Dinge behalten sind alle anderen Felder. Sie können all dies auch in die _id , oder Sie können $first .

Damit gibt es nur noch eine $group -Gruppenphase, um zum Venue selbst zurückzukehren. Diesmal lautet der Gruppierungsschlüssel natürlich "$_id" , wobei alle Eigenschaften des Veranstaltungsorts selbst $first und die verbleibenden "$review" "$_id" mit $push in ein Array zurückgehen. Natürlich wird die Ausgabe von "$comments" aus der vorherigen $group zum Pfad "review.comments" .

Das Arbeiten an einem einzelnen Dokument und seinen Beziehungen ist nicht wirklich so schlimm. Der $unwind Pipeline-Operator kann im Allgemeinen ein Leistungsproblem darstellen, sollte jedoch im Zusammenhang mit dieser Verwendung keine so großen Auswirkungen haben.

Da die Daten immer noch "auf dem Server zusammengeführt" werden, ist der Datenverkehr immer noch weitaus geringer als bei der anderen verbleibenden Alternative.

JavaScript-Manipulation

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

Das Problem bei der Verwendung von $lookup ist natürlich, dass es, obwohl es wie ein viel vereinfachterer Prozess 'aussieht' , tatsächlich in keiner Weise ein JOIN ist . Alles, was $lookup tatsächlich tut, ist , den zugrunde liegenden Prozess des Übergebens mehrerer Abfragen an die Datenbank zu "verbergen" und dann die Ergebnisse durch asynchrone Verarbeitung abzuwarten.

Das "Erscheinen" eines Joins ist also das Ergebnis mehrerer Anfragen an den Server und anschließender "clientseitiger Manipulation" der Daten, um die Details in Arrays einzubetten.

Abgesehen von dieser eindeutigen Warnung, dass die Leistungsmerkmale bei weitem nicht mit einem Server- $lookup , besteht die andere Einschränkung natürlich darin, dass die "Mungo-Dokumente" im Ergebnis keine reinen JavaScript-Objekte sind, die einer weiteren Manipulation unterliegen.

Um diesen Ansatz zu .lean() , müssen Sie der Abfrage vor der Ausführung die Methode .lean() hinzufügen, um .lean() anzuweisen, "einfache JavaScript-Objekte" anstelle von Document , die mit an das Modell angehängten Schemamethoden umgewandelt werden . Festzustellen ist natürlich, dass die resultierenden Daten keinen Zugriff mehr auf "Instanzmethoden" haben, die ansonsten mit den zugehörigen 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 ist der venue ein einfaches Objekt, das wir einfach nach Bedarf bearbeiten und anpassen können:

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 zu radeln, bis die Ebene erreicht ist, auf der Sie das followers Array in den Details des author . Der Vergleich kann dann mit den in diesem Array gespeicherten ObjectId Werten durchgeführt werden, nachdem zuerst .map() , um die "string" -Werte zum Vergleich mit der req.user.id die ebenfalls ein String ist (wenn nicht, dann auch) füge dazu .toString() , da es im Allgemeinen einfacher ist, diese Werte auf diese Weise über JavaScript-Code zu vergleichen.

Wieder muss ich betonen, dass es "einfach aussieht", aber es ist in der Tat die Art von Dingen, die Sie wirklich für die Systemleistung vermeiden möchten, da diese zusätzlichen Abfragen und die Übertragung zwischen dem Server und dem Client viel Zeit in der Verarbeitung kosten und dies summiert sich sogar aufgrund des Anforderungsaufwands zu tatsächlichen Kosten beim Transport zwischen Hosting-Anbietern.

Zusammenfassung

Dies sind im Grunde Ihre Ansätze, die Sie $lookup , $lookup von "Rolling Your Own", bei dem Sie die "Mehrfachabfragen" an die Datenbank selbst durchführen, anstatt den Helfer zu verwenden, der $lookup ist.

Mit der Populate-Ausgabe können Sie die Daten im Ergebnis dann einfach wie jede andere Datenstruktur .lean() sofern Sie .lean() auf die Abfrage anwenden, um die .lean() Objektdaten aus den zurückgegebenen .lean() zu konvertieren oder auf andere Weise zu extrahieren.

Während die aggregierten Ansätze weitaus komplexer aussehen, hat diese Arbeit auf dem Server "viel" mehr Vorteile. Größere Ergebnismengen können sortiert werden, Berechnungen für die weitere Filterung können durchgeführt werden, und natürlich erhalten Sie eine "einzelne Antwort" auf eine "einzelne Anforderung", die an den Server gesendet wird, und dies alles ohne zusätzlichen Aufwand.

Es ist absolut umstritten, dass die Pipelines selbst einfach auf der Grundlage von Attributen erstellt werden könnten, die bereits im Schema gespeichert sind. Daher sollte es nicht allzu schwierig sein, eine eigene Methode zu schreiben, um diese "Konstruktion" basierend auf dem angehängten Schema durchzuführen.

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

Ich habe folgende Sammlungen

Veranstaltungsort Sammlung

{
    "_id" : ObjectId("5acdb8f65ea63a27c1facf86"),
    "name" : "ASA College - Manhattan Campus",
    "addedBy" : ObjectId("5ac8ba3582c2345af70d4658"),
    "reviews" : [ 
        ObjectId("5acdb8f65ea63a27c1facf8b"), 
        ObjectId("5ad8288ccdd9241781dce698")
    ]
}

Bewertungen Sammlung

{
    "_id" : ObjectId("5acdb8f65ea63a27c1facf8b"),
    "createdAt" : ISODate("2018-04-07T12:31:49.503Z"),
    "venue" : ObjectId("5acdb8f65ea63a27c1facf86"),
    "author" : ObjectId("5ac8ba3582c2345af70d4658"),
    "content" : "nice place",
    "comments" : [ 
        ObjectId("5ad87113882d445c5cbc92c8")
    ],
}

Kommentarsammlung

{
    "_id" : ObjectId("5ad87113882d445c5cbc92c8"),
    "author" : ObjectId("5ac8ba3582c2345af70d4658"),
    "comment" : "dcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsf",
    "review" : ObjectId("5acdb8f65ea63a27c1facf8b"),
    "__v" : 0
}

Autorensammlung

{
    "_id" : ObjectId("5ac8ba3582c2345af70d4658"),
    "firstName" : "Bruce",
    "lastName" : "Wayne",
    "email" : "[email protected]",
    "followers" : [ObjectId("5ac8b91482c2345af70d4650")]
}

Jetzt funktioniert meine folgende Bestandsabfrage problemlos

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

Aber ich möchte es mit $lookup query erreichen, aber es teilt den Ort auf, wenn ich '$ unwind' für die Reviews mache ... Ich möchte Reviews in demselben Array (wie populate) und in derselben Reihenfolge ...

Ich möchte die folgende Abfrage mit $lookup da der Autor ein Follower-Feld hat. isFollow muss das Feld isFollow werden, indem $project was mit populate .

$project: {
    isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] }
}




aggregation-framework