sql >> Database >  >> NoSQL >> MongoDB

Retourneer alleen overeenkomende subdocumentelementen binnen een geneste array

Dus de query die je hebt, selecteert het "document" precies zoals het hoort. Maar waar u naar op zoek bent, is om "de arrays te filteren" die zijn opgenomen, zodat de geretourneerde elementen alleen overeenkomen met de voorwaarde van de query.

Het echte antwoord is natuurlijk dat, tenzij je echt veel bandbreedte bespaart door dergelijke details eruit te filteren, je het niet eens moet proberen, of in ieder geval na de eerste positionele match.

MongoDB heeft een positionele $ operator die een array-element retourneert bij de overeenkomende index van een queryvoorwaarde. Dit retourneert echter alleen de "eerste" overeenkomende index van het "buitenste" array-element.

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

In dit geval betekent het de "stores" alleen array-positie. Dus als er meerdere "winkels"-items waren, zou slechts "één" van de elementen die uw overeenkomende voorwaarde bevatten, worden geretourneerd. Maar , dat doet niets voor de binnenste reeks van "offers" , en als zodanig elke "aanbieding" binnen de overeenkomende "stores" array zou nog steeds worden geretourneerd.

MongoDB kan dit niet "filteren" in een standaardquery, dus het volgende werkt niet:

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

De enige tools die MongoDB eigenlijk heeft om dit niveau van manipulatie uit te voeren, is met het aggregatieraamwerk. Maar de analyse zou u moeten laten zien waarom u dit "waarschijnlijk" niet zou moeten doen, en in plaats daarvan de array in code zou moeten filteren.

In volgorde van hoe je dit per versie kunt bereiken.

Eerst met MongoDB 3.2.x met gebruik van het $filter bediening:

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", [] ]}
      }
    }
  }}
])

Daarna met MongoDB 2.6.x en hoger met $map en $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]
      ]
    }
  }}
])

En tot slot in elke versie hierboven MongoDB 2.2.x waar het aggregatieraamwerk werd geïntroduceerd.

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"
      }
    }
  }}
])

Laten we de uitleg opsplitsen.

MongoDB 3.2.x en hoger

Dus in het algemeen, $filter is de manier om hier naartoe te gaan, omdat het is ontworpen met het doel voor ogen. Aangezien er meerdere niveaus van de array zijn, moet u dit op elk niveau toepassen. Dus eerst duik je in elke "offers" binnen "stores" om te onderzoeken en $filter die inhoud.

De eenvoudige vergelijking hier is "Heeft de "size" array bevat het element waarnaar ik op zoek ben" . In deze logische context is het korte wat je moet doen de $setIsSubset bewerking om een ​​array ("set") van ["L"] . te vergelijken naar de doelreeks. Waar die voorwaarde true is ( het bevat "L") en vervolgens het array-element voor "offers" wordt behouden en geretourneerd in het resultaat.

Op het hogere niveau $filter , dan kijkt u of het resultaat van dat vorige $filter retourneerde een lege array [] voor "offers" . Als het niet leeg is, wordt het element geretourneerd of anders wordt het verwijderd.

MongoDB 2.6.x

Dit lijkt erg op het moderne proces, behalve dat er geen $filter . is in deze versie kun je $map . gebruiken om elk element te inspecteren en vervolgens $setDifference . te gebruiken om alle elementen uit te filteren die zijn geretourneerd als false .

Dus $map gaat de hele array retourneren, maar de $cond operatie beslist alleen of het element wordt geretourneerd of in plaats daarvan een false waarde. In de vergelijking van $setDifference naar een enkel element "set" van [false] allemaal false elementen in de geretourneerde array zouden worden verwijderd.

Op alle andere manieren is de logica hetzelfde als hierboven.

MongoDB 2.2.x en hoger

Dus onder MongoDB 2.6 is de enige tool om met arrays te werken $unwind , en alleen voor dit doel moet u niet gebruik hiervoor het aggregatieraamwerk "slechts".

Het proces lijkt inderdaad eenvoudig, door simpelweg elke array "uit elkaar te halen", de dingen eruit te filteren die je niet nodig hebt en deze vervolgens weer in elkaar te zetten. De belangrijkste zorg is in de "twee" $group fasen, met de "eerste" om de binnenste array opnieuw te bouwen en de volgende om de buitenste array opnieuw te bouwen. Er zijn verschillende _id waarden op alle niveaus, dus deze hoeven alleen maar op elk groeperingsniveau te worden opgenomen.

Maar het probleem is dat $unwind is erg duur . Hoewel het nog steeds een doel heeft, is het belangrijkste gebruiksdoel niet om dit soort filtering per document uit te voeren. In moderne releases mag het eigenlijk alleen worden gebruikt wanneer een element van de array(s) deel moet gaan uitmaken van de "groeperingssleutel" zelf.

Conclusie

Het is dus geen eenvoudig proces om overeenkomsten op meerdere niveaus van een array als deze te krijgen, en in feite kan het extreem duur zijn indien onjuist geïmplementeerd.

Alleen de twee moderne lijsten mogen ooit voor dit doel worden gebruikt, omdat ze een "enkele" pijplijnfase gebruiken naast de "query" $match om het "filteren" uit te voeren. Het resulterende effect is weinig meer overhead dan de standaardvormen van .find() .

Over het algemeen zijn die lijsten echter nog steeds een beetje ingewikkeld, en inderdaad, tenzij je de inhoud die door dergelijke filtering wordt geretourneerd echt drastisch vermindert op een manier die een aanzienlijke verbetering oplevert in de bandbreedte die wordt gebruikt tussen de server en de client, dan ben je beter van het filteren van het resultaat van de initiële zoekopdracht en basisprojectie.

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

Dus het werken met het geretourneerde object "post"-queryverwerking is veel minder stomp dan het gebruik van de aggregatiepijplijn om dit te doen. En zoals gezegd zou het enige "echte" verschil zijn dat u de andere elementen op de "server" weggooit in plaats van ze "per document" te verwijderen wanneer ze worden ontvangen, wat een beetje bandbreedte kan besparen.

Maar tenzij je dit doet in een moderne versie met alleen $match en $project , dan zullen de "kosten" van verwerking op de server ruimschoots opwegen tegen de "winst" van het verminderen van die netwerkoverhead door eerst de ongeëvenaarde elementen te verwijderen.

In alle gevallen krijg je hetzelfde resultaat:

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


  1. mongodb, repliceert en fout:{$err:niet master en slaveOk=false, code:13435}

  2. MongoDB $arrayElemAt

  3. Node.js Redis Connection Pooling

  4. MongoDB aan met Docker kon geen verbinding maken met server [localhost:27017] bij eerste verbinding