Така че заявката, която имате, всъщност избира "документа" точно както трябва. Но това, което търсите, е да „филтрирате масивите“, които се съдържат, така че върнатите елементи да отговарят само на условието на заявката.
Истинският отговор, разбира се, е, че освен ако наистина не спестявате много честотна лента чрез филтриране на такива подробности, тогава дори не трябва да опитвате или поне след първото позиционно съвпадение.
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"
]
}
]
}
]
}