Die Hauptkollektion ist der Einzelhändler, der ein Array für Geschäfte enthält. Jedes Geschäft enthält eine Reihe von Angeboten (Sie können in diesem Geschäft kaufen). Dieses Angebot Array hat eine Reihe von Größen. (Siehe Beispiel unten)
Jetzt versuche ich alle Angebote zu finden, die in der Größe verfügbar sind L
.
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"XS",
"S",
"M"
]
},
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
Ich habe diese Abfrage versucht: db.getCollection('retailers').find({'stores.offers.size': 'L'})
Ich erwarte eine solche Ausgabe:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
Die Ausgabe meiner Abfrage enthält aber auch das nicht übereinstimmende Angebot mit size
XS, X und M.
Wie kann ich MongoDB zwingen, nur die Angebote zurückzugeben, die meiner Anfrage entsprechen?
Grüße und danke.
db.getCollection('retailers').find({'stores.offers.size': 'L'}, {'stores.offers': 1})
. Aber dann enthält die Antwort auch falsche Angebote$match
und$unwind
für mein Problem verwenden?Antworten:
Die Abfrage, die Sie haben, wählt also das "Dokument" genau so aus, wie es sollte. Sie suchen jedoch nach "Filtern der enthaltenen Arrays", sodass die zurückgegebenen Elemente nur der Bedingung der Abfrage entsprechen.
Die eigentliche Antwort lautet natürlich: Wenn Sie nicht wirklich viel Bandbreite sparen, indem Sie solche Details herausfiltern, sollten Sie es nicht einmal versuchen oder zumindest über die erste Positionsübereinstimmung hinaus.
MongoDB verfügt über einen Positionsoperator
$
, 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 dies nur die
"stores"
Array-Position. Wenn also mehrere "Speicher" -Einträge vorhanden wären, würde nur "eines" der Elemente zurückgegeben, die Ihre übereinstimmende Bedingung enthielten. Dies bedeutet jedoch nichts für das innere Array von"offers"
, und als solches würde jedes "Angebot" innerhalb des übereinstimmenden"stores"
Arrays immer noch zurückgegeben.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 } )
Das einzige Werkzeug, das MongoDB tatsächlich benötigt, um diese Manipulationsebene auszuführen, ist das Aggregationsframework. Die Analyse sollte Ihnen jedoch zeigen, warum Sie dies "wahrscheinlich" nicht tun sollten, und stattdessen nur das Array im Code filtern.
In der Reihenfolge, wie Sie dies pro Version erreichen können.
Zuerst mit MongoDB 3.2.x mit der
$filter
Operation: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, in der das Aggregationsframework 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
Im Allgemeinen
$filter
ist dies der richtige Weg, da es auf den Zweck ausgelegt ist. Da es mehrere Ebenen des Arrays gibt, müssen Sie dies auf jeder Ebene anwenden. Also zunächst sind Sie tauchen in jeden"offers"
innerhalb"stores"
zu examime und$filter
diese Inhalte.Der einfache Vergleich hier lautet "Enthält das
"size"
Array das gesuchte Element?" . In diesem logischen Kontext besteht die kurze Aufgabe darin, die$setIsSubset
Operation zum Vergleichen eines Arrays ("Set")["L"]
mit dem Zielarray zu verwenden. Wenn diese Bedingung erfüllt isttrue
(sie enthält "L"), wird das Array-Element für"offers"
beibehalten und im Ergebnis zurückgegeben.In der höheren Ebene
$filter
suchen Sie dann, ob das Ergebnis dieses vorherigen$filter
ein leeres Array[]
für zurückgegeben hat"offers"
. Wenn es nicht leer ist, wird das Element zurückgegeben oder auf andere Weise entfernt.MongoDB 2.6.x.
Dies ist dem modernen Prozess sehr ähnlich, außer dass Sie, da es
$filter
in dieser Version keine gibt$map
, jedes Element überprüfen und dann$setDifference
alle Elemente herausfiltern können, die als zurückgegeben wurdenfalse
.Es wird also
$map
das gesamte Array zurückgegeben, aber die$cond
Operation entscheidet nur, ob das Element oder stattdessen einfalse
Wert zurückgegeben wird. Beim Vergleich$setDifference
mit einem einzelnen Element würde "Menge"[false]
allerfalse
Elemente im zurückgegebenen Array entfernt.In allen anderen Fällen ist die Logik dieselbe wie oben.
MongoDB 2.2.x und höher
So unter MongoDB 2.6 das einzige Werkzeug für mit Arrays arbeiten ist
$unwind
, und zu diesem Zweck allein sollten Sie nicht die Aggregation Rahmen „nur“ für diesen Zweck verwenden.Der Prozess erscheint in der Tat einfach, indem Sie einfach jedes Array "auseinander nehmen", die Dinge herausfiltern, die Sie nicht benötigen, und es dann wieder zusammensetzen. Die Hauptaufgabe liegt in den "zwei"
$group
Phasen, wobei die "erste" das innere Array neu erstellt und die nächste das äußere Array neu erstellt. Auf_id
allen Ebenen gibt es unterschiedliche Werte, daher müssen diese nur auf jeder Gruppierungsebene berücksichtigt werden.Aber das Problem ist , dass
$unwind
ist sehr teuer . Obwohl es immer noch einen Zweck hat, besteht seine Hauptverwendungsabsicht darin, diese Art der Filterung nicht pro Dokument durchzuführen. Tatsächlich sollte es in modernen Releases nur verwendet werden, wenn ein Element des Arrays Teil des "Gruppierungsschlüssels" selbst werden muss.Fazit
Es ist also kein einfacher Prozess, Übereinstimmungen auf mehreren Ebenen eines Arrays wie dieses zu erhalten, und tatsächlich kann es bei falscher Implementierung äußerst kostspielig sein .
Zu diesem Zweck sollten immer nur die beiden modernen Listings verwendet werden, da sie zusätzlich zur "Abfrage" eine "einzelne" Pipeline-Stufe
$match
verwenden, um die "Filterung" durchzuführen. Der resultierende Effekt ist wenig mehr Aufwand als die Standardformen von.find()
.Im Allgemeinen sind diese Einträge jedoch immer noch sehr komplex. Wenn Sie den durch eine solche Filterung zurückgegebenen Inhalt nicht drastisch reduzieren, um die zwischen Server und Client verwendete Bandbreite erheblich zu verbessern, sind Sie besser des Filterns des Ergebnisses der anfänglichen Abfrage und der 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); })
Die Arbeit mit der Abfrageverarbeitung des zurückgegebenen Objekts "post" ist daher weitaus weniger stumpf als die Verwendung der Aggregationspipeline, um dies zu tun. Und wie bereits erwähnt, besteht der einzige "echte" Unterschied darin, dass Sie die anderen Elemente auf dem "Server" verwerfen, anstatt sie beim Empfang "pro Dokument" zu entfernen, was möglicherweise ein wenig Bandbreite spart.
Wenn Sie dies jedoch nicht in einer modernen Version mit nur
$match
und tun,$project
überwiegen die "Kosten" für die Verarbeitung auf dem Server erheblich den "Gewinn" bei der Reduzierung des Netzwerk-Overheads, indem zuerst die nicht übereinstimmenden Elemente entfernt werden.In allen Fällen erhalten Sie das gleiche Ergebnis:
{ "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size" : [ "S", "L", "XL" ] } ] } ] }
quelle
Da Ihr Array eingebettet ist, können wir $ elemMatch nicht verwenden. Stattdessen können Sie das Aggregationsframework verwenden, um Ihre Ergebnisse zu erhalten:
db.retailers.aggregate([ {$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped {$unwind:"$stores"}, {$unwind:"$stores.offers"}, {$match:{"stores.offers.size": 'L'}}, {$group:{ _id:{id:"$_id", "storesId":"$stores._id"}, "offers":{$push:"$stores.offers"} }}, {$group:{ _id:"$_id.id", stores:{$push:{_id:"$_id.storesId","offers":"$offers"}} }} ]).pretty()
Bei dieser Abfrage werden Arrays (zweimal) abgewickelt, die Größe angepasst und das Dokument in das vorherige Formular umgeformt. Sie können $ group-Schritte entfernen und sehen, wie sie gedruckt werden. Viel Spaß!
quelle