sql >> Databasteknik >  >> NoSQL >> MongoDB

Returnera endast matchade underdokumentelement inom en kapslad array

Så frågan du har väljer faktiskt "dokumentet" precis som det ska. Men det du letar efter är att "filtrera arrayerna" som finns så att de returnerade elementen endast matchar villkoret för frågan.

Det verkliga svaret är naturligtvis att om du inte verkligen sparar mycket bandbredd genom att filtrera bort sådana detaljer så bör du inte ens försöka, eller åtminstone efter den första positionsmatchningen.

MongoDB har en positionell $ operator som kommer att returnera ett arrayelement vid det matchade indexet från ett frågevillkor. Detta returnerar dock bara det "första" matchade indexet för det "yttre" mest arrayelementet.

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

I det här fallet betyder det "stores" endast arrayposition. Så om det fanns flera "butiker"-poster skulle bara "en" av elementen som innehöll ditt matchade villkor returneras. Men , som inte gör något för den inre arrayen av "offers" , och som sådan varje "erbjudande" i de matchade "stores" array skulle fortfarande returneras.

MongoDB har inget sätt att "filtrera" detta i en standardfråga, så följande fungerar inte:

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

De enda verktyg som MongoDB faktiskt har för att göra denna nivå av manipulation är med aggregeringsramverket. Men analysen borde visa dig varför du "förmodligen" inte borde göra detta, och istället bara filtrera arrayen i kod.

I ordning hur du kan uppnå detta per version.

Först med MongoDB 3.2.x med hjälp av $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", [] ]}
      }
    }
  }}
])

Sedan med MongoDB 2.6.x och över med $map och $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]
      ]
    }
  }}
])

Och slutligen i valfri version ovan MongoDB 2.2.x där aggregeringsramverket infördes.

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

Låt oss bryta ner förklaringarna.

MongoDB 3.2.x och senare

Så generellt sett, $filter är vägen att gå här eftersom den är designad med syftet i åtanke. Eftersom det finns flera nivåer i arrayen måste du tillämpa detta på varje nivå. Så först dyker du in i varje "offers" i "stores" för att undersöka och $filter det innehållet.

Den enkla jämförelsen här är "Är "size" array innehåller elementet jag letar efter" . I detta logiska sammanhang är det korta att göra att använda $setIsSubset operation för att jämföra en array ("uppsättning") av ["L"] till målarrayen. Där det villkoret är true ( den innehåller "L") sedan arrayelementet för "offers" behålls och returneras i resultatet.

På den högre nivån $filter , letar du sedan efter resultatet från det tidigare $filter returnerade en tom array [] för "offers" . Om det inte är tomt returneras elementet eller på annat sätt tas det bort.

MongoDB 2.6.x

Detta är mycket likt den moderna processen förutom att eftersom det inte finns något $filter i den här versionen kan du använda $map för att inspektera varje element och använd sedan $setDifference för att filtrera bort alla element som returnerades som false .

$map kommer att returnera hela arrayen, men $cond operationen avgör bara om elementet ska returneras eller istället en false värde. I jämförelsen av $setDifference till en enstaka element "uppsättning" av [false] alla false element i den returnerade arrayen skulle tas bort.

På alla andra sätt är logiken densamma som ovan.

MongoDB 2.2.x och uppåt

Så under MongoDB 2.6 är det enda verktyget för att arbeta med arrayer $unwind , och bara för detta ändamål bör du inte använd aggregeringsramverket "bara" för detta ändamål.

Processen verkar verkligen enkel, genom att helt enkelt "ta isär" varje array, filtrera bort de saker du inte behöver och sedan sätta ihop den igen. Den huvudsakliga behandlingen är i "två" $group steg, med den "förste" att bygga om den inre arrayen och nästa att bygga om den yttre arrayen. Det finns distinkta _id värden på alla nivåer, så dessa behöver bara inkluderas på alla grupperingsnivåer.

Men problemet är att $unwind är mycket kostsamt . Även om det fortfarande har ett syfte, är dess huvudsakliga användning inte att göra den här typen av filtrering per dokument. I moderna utgåvor bör det bara användas när ett element i arrayen/matriserna behöver bli en del av själva "grupperingsnyckeln".

Slutsats

Så det är inte en enkel process att få matchningar på flera nivåer av en array som denna, och det kan faktiskt vara extremt kostsamt om det implementeras felaktigt.

Endast de två moderna listorna bör någonsin användas för detta ändamål, eftersom de använder ett "enkelt" pipelinesteg utöver "frågan" $match för att göra "filtreringen". Den resulterande effekten är lite mer overhead än standardformerna för .find() .

Men i allmänhet har dessa listor fortfarande en viss komplexitet, och om du verkligen inte drastiskt minskar innehållet som returneras av sådan filtrering på ett sätt som gör en betydande förbättring av bandbredden som används mellan servern och klienten, då är du bättre att filtrera resultatet av den initiala frågan och grundläggande projektion.

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

Så att arbeta med det returnerade objektet "post"-förfrågebehandling är mycket mindre trubbigt än att använda aggregeringspipelinen för att göra detta. Och som sagt är den enda "riktiga" skillnaden att du kasserar de andra elementen på "servern" istället för att ta bort dem "per dokument" när de tas emot, vilket kan spara lite bandbredd.

Men om du inte gör detta i en modern version med endast $match och $project , då kommer "kostnaden" för bearbetning på servern att avsevärt uppväga "vinsten" av att minska nätverkskostnaderna genom att först ta bort de oöverträffade elementen.

I alla fall får du samma resultat:

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


  1. Övervakning och operationshantering av MongoDB 4.0 med ClusterControl

  2. Så här ansluter du till dina MongoDB-distributioner med hjälp av Robo 3T GUI

  3. Hur man läser flera uppsättningar lagrade på Redis med hjälp av något kommando eller LUA-skript

  4. Tvingar tillämpningen av ett 2dsphere-index på ett mongoose-schema att platsfältet krävs?