MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Върнете само съвпадащи елементи на поддокумент в рамките на вложен масив

Така че заявката, която имате, всъщност избира "документа" точно както трябва. Но това, което търсите, е да „филтрирате масивите“, които се съдържат, така че върнатите елементи да отговарят само на условието на заявката.

Истинският отговор, разбира се, е, че освен ако наистина не спестявате много честотна лента чрез филтриране на такива подробности, тогава дори не трябва да опитвате или поне след първото позиционно съвпадение.

MongoDB има позиционен $ оператор, който ще върне елемент на масив със съвпадащия индекс от условие на заявка. Това обаче връща само "първия" съответстващ индекс на най-външния елемент на масива.

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

В този случай това означава "stores" само позиция на масива. Така че, ако имаше няколко записа за "магазини", тогава ще бъде върнат само "един" от елементите, които съдържат вашето съвпадение. Но , което не прави нищо за вътрешния масив на "offers" , и като такъв всяка „оферта“ в рамките на съвпадащите "stores" масивът пак ще бъде върнат.

MongoDB няма начин да „филтрира“ това в стандартна заявка, така че следното не работи:

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

Единствените инструменти, които MongoDB всъщност има за това ниво на манипулация, е с рамката за агрегиране. Но анализът трябва да ви покаже защо „вероятно“ не трябва да правите това, а вместо това просто да филтрирате масива в код.

По реда на това как можете да постигнете това за всяка версия.

Първо сMongoDB 3.2.x с помощта на $filter операция:

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

След това сMongoDB 2.6.x и по-горе с $map и $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]
      ]
    }
  }}
])

И накрая във всяка версия над MongoDB 2.2.x където е въведена рамката за агрегиране.

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

Нека разбием обясненията.

MongoDB 3.2.x и по-нови версии

Така най-общо казано, $filter е начинът да отидете тук, тъй като е проектиран с целта. Тъй като има множество нива на масива, трябва да приложите това на всяко ниво. Така че първо се гмуркате във всяка "offers" в рамките на "stores" to examime и $filter това съдържание.

Простото сравнение тук е „Има ли "size" масив съдържа елемента, който търся" . В този логически контекст най-краткото нещо, което трябва да направите, е да използвате $setIsSubset операция за сравняване на масив ("набор") от ["L"] към целевия масив. Когато това условие е true (съдържа "L"), след това елемента на масива за "offers" се запазва и се връща в резултата.

В по-високо ниво $filter , след това търсите да видите дали резултатът от предишния $filter върна празен масив [] за "offers" . Ако не е празен, тогава елементът се връща или в противен случай се премахва.

MongoDB 2.6.x

Това е много подобно на съвременния процес, с изключение на това, че няма $filter в тази версия можете да използвате $map за да проверите всеки елемент и след това използвайте $setDifference за да филтрирате всички елементи, които са върнати като false .

Така че $map ще върне целия масив, но $cond операцията просто решава дали да върне елемента или вместо това false стойност. В сравнението на $setDifference към един елемент "набор" от [false] всички false елементи от върнатия масив ще бъдат премахнати.

Във всички останали начини логиката е същата като по-горе.

MongoDB 2.2.x и по-нови версии

Така че под MongoDB 2.6 единственият инструмент за работа с масиви е $unwind , а само за тази цел трябва дане използвайте рамката за агрегиране "само" за тази цел.

Процесът наистина изглежда прост, като просто "разглобявате" всеки масив, филтрирате нещата, от които не се нуждаете, след което го събирате обратно. Основната грижа е в "двете" $group етапи, като "първият" да изгради отново вътрешния масив, а следващият да изгради отново външния масив. Има различни _id стойности на всички нива, така че те просто трябва да бъдат включени на всяко ниво на групиране.

Но проблемът е, че $unwind е много скъпо . Въпреки че все още има цел, основното му предназначение е да не прави този вид филтриране по документ. Всъщност в съвременните версии това трябва да се използва само когато елемент от масива(ите) трябва да стане част от самия „ключ за групиране“.

Заключение

Така че не е лесен процес да получите съвпадения на множество нива на масив като този и всъщност може да бъде изключително скъпо ако се прилага неправилно.

За тази цел трябва да се използват само двата съвременни списъка, тъй като те използват "единичен" етап на конвейера в допълнение към "заявката" $match за да направите "филтрирането". Полученият ефект е малко повече от стандартните форми на .find() .

Като цяло обаче тези обяви все още имат известна сложност и наистина, освен ако наистина не намалите драстично съдържанието, връщано от такова филтриране по начин, който прави значително подобрение в използваната честотна лента между сървъра и клиента, тогава сте по-добре на филтриране на резултата от първоначалната заявка и основна проекция.

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

Така че работата с върнатия обект "след" обработка на заявка е далеч по-малко тъпа, отколкото използването на конвейера за агрегиране за това. И както беше посочено, единствената „реална“ разлика би била, че изхвърляте другите елементи на „сървъра“, вместо да ги премахвате „на документ“, когато бъдат получени, което може да спести малко честотна лента.

Но освен ако не правите това в модерна версия ссамо $match и $project , тогава „цената“ на обработката на сървъра значително ще надхвърли „печалбата“ от намаляване на режийните разходи на мрежата чрез премахване на несравнимите елементи първо.

Във всички случаи получавате един и същ резултат:

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


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Mongodb $lookup динамична колекция

  2. Хибернация с MongoDB

  3. Автоматично премахване на препращащи обекти при изтриване в MongoDB

  4. Как да изхвърлите база данни в MongoDB от командния ред

  5. mgo - производителността на заявката изглежда постоянно бавна (500-650ms)