Отже, запит, який ви маєте, фактично вибирає "документ", як слід. Але те, що ви шукаєте, - це "відфільтрувати масиви", що містяться таким чином, щоб повернуті елементи відповідали лише умові запиту.
Дійсна відповідь, звичайно, полягає в тому, що якщо ви дійсно не економите багато пропускної здатності, відфільтрувавши такі деталі, тоді вам навіть не слід намагатися, або, принаймні, після першого позиційного збігу.
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"
іспиту та $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) {
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"
]
}
]
}
]
}
db.getCollection('retailers').find({'stores.offers.size': 'L'}, {'stores.offers': 1})
. Але тоді відповідь також містить неправильні пропозиції