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

Gibt nur übereinstimmende Unterdokumentelemente innerhalb eines verschachtelten Arrays zurück

Die Abfrage, die Sie haben, wählt also das "Dokument" genau so aus, wie es sollte. Was Sie aber suchen, ist "die enthaltenen Arrays zu filtern", sodass die zurückgegebenen Elemente nur der Bedingung der Abfrage entsprechen.

Die wirkliche Antwort ist natürlich, dass Sie es nicht einmal versuchen sollten, es sei denn, Sie sparen wirklich viel Bandbreite, indem Sie solche Details herausfiltern, oder zumindest über die erste Positionsübereinstimmung hinaus.

MongoDB hat einen positionellen $ -Operator, der ein Array-Element am übereinstimmenden Index aus einer Abfragebedingung zurückgibt. Dies gibt jedoch nur den "ersten" übereinstimmenden Index des "äußersten" Array-Elements zurück.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)

In diesem Fall bedeutet es "stores" Nur Array-Position. Wenn also mehrere „stores“-Einträge vorhanden sind, wird nur „eines“ der Elemente zurückgegeben, das Ihre übereinstimmende Bedingung enthält. Aber , das tut nichts für das innere Array von "offers" , und damit jedes "Angebot" innerhalb der übereinstimmenden "stores" array würde trotzdem zurückgegeben werden.

MongoDB hat keine Möglichkeit, dies in einer Standardabfrage zu „filtern“, daher funktioniert Folgendes nicht:

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$.offers.$': 1 }
)

Die einzigen Tools, die MongoDB tatsächlich hat, um diese Manipulationsebene durchzuführen, sind das Aggregations-Framework. Aber die Analyse sollte Ihnen zeigen, warum Sie dies „wahrscheinlich“ nicht tun sollten und stattdessen das Array einfach im Code filtern sollten.

In der Reihenfolge, wie Sie dies pro Version erreichen können.

Zuerst mit MongoDB 3.2.x mit dem $filter Betrieb:

db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$filter": {
        "input": {
          "$map": {
            "input": "$stores",
            "as": "store",
            "in": {
              "_id": "$$store._id",
              "offers": {
                "$filter": {
                  "input": "$$store.offers",
                  "as": "offer",
                  "cond": {
                    "$setIsSubset":  [ ["L"], "$$offer.size" ]
                  }
                }
              }
            }
          }
        },
        "as": "store",
        "cond": { "$ne": [ "$$store.offers", [] ]}
      }
    }
  }}
])

Dann mit MongoDB 2.6.x und höher mit $map und $setDifference :

db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$setDifference": [
        { "$map": {
          "input": {
            "$map": {
              "input": "$stores",
              "as": "store",
              "in": {
                "_id": "$$store._id",
                "offers": {
                  "$setDifference": [
                    { "$map": {
                      "input": "$$store.offers",
                      "as": "offer",
                      "in": {
                        "$cond": {
                          "if": { "$setIsSubset": [ ["L"], "$$offer.size" ] },
                          "then": "$$offer",
                          "else": false
                        }
                      }
                    }},
                    [false]
                  ]
                }
              }
            }
          },
          "as": "store",
          "in": {
            "$cond": {
              "if": { "$ne": [ "$$store.offers", [] ] },
              "then": "$$store",
              "else": false
            }
          }
        }},
        [false]
      ]
    }
  }}
])

Und schließlich in jeder Version über MongoDB 2.2.x wo das Aggregations-Framework eingeführt wurde.

db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$unwind": "$stores" },
  { "$unwind": "$stores.offers" },
  { "$match": { "stores.offers.size": "L" } },
  { "$group": {
    "_id": {
      "_id": "$_id",
      "storeId": "$stores._id",
    },
    "offers": { "$push": "$stores.offers" }
  }},
  { "$group": {
    "_id": "$_id._id",
    "stores": {
      "$push": {
        "_id": "$_id.storeId",
        "offers": "$offers"
      }
    }
  }}
])

Lassen Sie uns die Erklärungen aufschlüsseln.

MongoDB 3.2.x und höher

Also allgemein gesagt $filter ist hier der richtige Weg, da es mit dem Zweck im Hinterkopf entworfen wurde. Da es mehrere Ebenen des Arrays gibt, müssen Sie dies auf jeder Ebene anwenden. Tauchen Sie also zuerst in die einzelnen "offers" ein innerhalb von "stores" zu prüfen und $filter diesen Inhalt.

Der einfache Vergleich hier ist "Entspricht der "size" Array enthält das Element, nach dem ich suche" . In diesem logischen Kontext ist die Kurzform, $setIsSubset zu verwenden Operation zum Vergleichen eines Arrays ("set") von ["L"] zum Zielarray. Wo diese Bedingung true ist (es enthält "L") dann das Array-Element für "offers" wird beibehalten und im Ergebnis zurückgegeben.

In der übergeordneten Ebene $filter , suchen Sie dann nach dem Ergebnis dieses vorherigen $filter hat ein leeres Array [] zurückgegeben für "offers" . Wenn es nicht leer ist, wird das Element zurückgegeben oder andernfalls entfernt.

MongoDB 2.6.x

Dies ist dem modernen Prozess sehr ähnlich, außer dass es keinen $filter gibt in dieser Version können Sie $map verwenden um jedes Element zu untersuchen und dann $setDifference zu verwenden um alle Elemente herauszufiltern, die als false zurückgegeben wurden .

Also $map wird das gesamte Array zurückgeben, aber $cond Die Operation entscheidet nur, ob das Element oder stattdessen ein false zurückgegeben wird Wert. Beim Vergleich von $setDifference zu einem einzelnen Element "set" von [false] alles false Elemente im zurückgegebenen Array würden entfernt.

Ansonsten ist die Logik die gleiche wie oben.

MongoDB 2.2.x und höher

Daher ist unter MongoDB 2.6 das einzige Tool zum Arbeiten mit Arrays $unwind , und allein zu diesem Zweck sollten Sie nicht Verwenden Sie das Aggregations-Framework "nur" für diesen Zweck.

Der Prozess scheint in der Tat einfach zu sein, indem Sie einfach jedes Array "auseinandernehmen", die Dinge herausfiltern, die Sie nicht benötigen, und es dann wieder zusammensetzen. Die Hauptpflege liegt in den "zwei" $group Phasen, wobei die "erste" das innere Array neu aufbaut und die nächste das äußere Array wiederherstellt. Es gibt eindeutige _id Werte auf allen Ebenen, sodass diese nur auf jeder Gruppierungsebene enthalten sein müssen.

Aber das Problem ist, dass $unwind ist sehr teuer . Obwohl es immer noch einen Zweck hat, besteht seine Hauptverwendungsabsicht nicht darin, diese Art der Filterung pro Dokument durchzuführen. Tatsächlich sollte es in modernen Versionen nur verwendet werden, wenn ein Element des Arrays (der Arrays) Teil des "Gruppierungsschlüssels" selbst werden muss.

Schlussfolgerung

Es ist also kein einfacher Prozess, Übereinstimmungen auf mehreren Ebenen eines Arrays wie diesem zu erhalten, und es kann tatsächlich extrem kostspielig sein bei falscher Implementierung.

Für diesen Zweck sollten immer nur die beiden modernen Auflistungen verwendet werden, da sie zusätzlich zur "Abfrage" $match eine "einzige" Pipelinestufe verwenden um das "Filtern" zu machen. Der resultierende Effekt ist etwas mehr Overhead als die Standardformen von .find() .

Im Allgemeinen sind diese Auflistungen jedoch immer noch etwas komplex, und wenn Sie den durch eine solche Filterung zurückgegebenen Inhalt nicht wirklich drastisch reduzieren, so dass die zwischen Server und Client verwendete Bandbreite erheblich verbessert wird, sind Sie besser des Filterns des Ergebnisses der ersten Abfrage und Grundprojektion.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
).forEach(function(doc) {
    // Technically this is only "one" store. So omit the projection
    // if you wanted more than "one" match
    doc.stores = doc.stores.filter(function(store) {
        store.offers = store.offers.filter(function(offer) {
            return offer.size.indexOf("L") != -1;
        });
        return store.offers.length != 0;
    });
    printjson(doc);
})

Daher ist die Arbeit mit der "Post"-Abfrageverarbeitung des zurückgegebenen Objekts weitaus weniger umständlich als die Verwendung der Aggregationspipeline, um dies zu tun. Und wie gesagt, der einzige "echte" Unterschied wäre, dass Sie die anderen Elemente auf dem "Server" verwerfen, anstatt sie "pro Dokument" zu entfernen, wenn sie empfangen werden, was etwas Bandbreite sparen kann.

Aber es sei denn, Sie tun dies in einer modernen Version mit nur $match und $project , dann überwiegen die "Kosten" der Verarbeitung auf dem Server den "Gewinn" der Reduzierung dieses Netzwerk-Overheads, indem zuerst die nicht übereinstimmenden Elemente entfernt werden.

In allen Fällen erhalten Sie dasselbe Ergebnis:

{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}