Основной ключ для работы с «именованными ключами», когда вы на самом деле не знаете, какие имена этих ключей заранее, использует $objectToArray
, который преобразует ваш объект в «ключ / значение» пары как элементы массива таким образом, что вы действительно можете работать с ними. Это современная функция MongoDB, добавленная в более поздних выпусках для MongoDB 3.4 и, конечно, во всех текущих будущих версиях.
Существует несколько подходов различной сложности и производительности.
Современный редукционный массив
db.collection.aggregate([
{ "$project": {
"sales": {
"$reduce": {
"input": {
"$map": {
"input": {
"$filter": {
"input": "$sales",
"cond": { "$not": "$$this.hidden" }
}
},
"in": { "$objectToArray": "$$this.salesTotals" }
}
},
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}
}
}},
{ "$unwind": "$sales" },
{ "$group": {
"_id": "$sales.k",
"v": { "$sum": "$sales.v" }
}},
{ "$group": {
"_id": null,
"data": { "$push": { "k": "$_id", "v": "$v" } }
}},
{ "$replaceRoot": {
"newRoot": { "$arrayToObject": "$data" }
}}
])
Использование $objectToArray
и обратное преобразование с помощью $arrayToObject
, так что на самом деле ни один из кодов не нуждается в «жестком кодировании» именованных ключей, на которых вы хотите накапливать.
$filter
по существу удаляет значения hidden
, а $map
преобразует только то, что вам нужно. $reduce
можно продолжить, но для накопления по документам вам все равно понадобится $unwind
позже.
Конечно, если вы просто имеете в виду «за документ», вы можете настроить это $reduce
намного дальше:
db.collection.aggregate([
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": [
{ "_id": "$_id" },
{
"$arrayToObject": {
"$reduce": {
"input": {
"$reduce": {
"input": {
"$map": {
"input": {
"$filter": {
"input": "$sales",
"cond": { "$not": "$$this.hidden" }
}
},
"in": { "$objectToArray": "$$this.salesTotals" }
}
},
"initialValue": [],
"in": {
"$concatArrays": [ "$$value", "$$this" ]
}
}
},
"initialValue": [],
"in": {
"$concatArrays": [
{ "$filter": {
"input": "$$value",
"as": "val",
"cond": { "$ne": [ "$$this.k", "$$val.k" ] }
}},
[{
"k": "$$this.k",
"v": {
"$cond": {
"if": { "$in": [ "$$this.k", "$$value.k" ] },
"then": {
"$sum": [
{ "$arrayElemAt": [
"$$value.v",
{ "$indexOfArray": [ "$$value.k", "$$this.k" ] }
]},
"$$this.v"
]
},
"else": "$$this.v"
}
}
}]
]
}
}
}
}
]
}
}}
])
Те же имена динамических ключей, но только для каждого документа, и в этом случае вам вообще не нужно $unwind
.
без $ уменьшить
Конечно, вы всегда можете делать такие вещи довольно традиционно:
db.collection.aggregate([
{ "$project": { "sales": "$sales" } },
{ "$unwind": "$sales" },
{ "$match": {
"sales.hidden": { "$ne": true }
}},
{ "$project": {
"sales": { "$objectToArray": "$sales.salesTotals" }
}},
{ "$unwind": "$sales" },
{ "$group": {
"_id": "$sales.k",
"v": { "$sum": "$sales.v" }
}},
{ "$group": {
"_id": null,
"data": { "$push": { "k": "$_id", "v": "$v" } }
}},
{ "$replaceRoot": {
"newRoot": { "$arrayToObject": "$data" }
}}
])
Это не выглядит сложным, но оно проходит через множество этапов, чтобы достичь результата. Таким образом, вместо $filter
вы $unwind
$match
, а вместо $map
вы делаете $project
только для требуемых свойств.
Нет необходимости объединять массивы в документах, потому что каждый $unwind
разбивает эти массивы на части.
В целом, это может быть просто и легко читаемо, но накладные расходы на выполнение значительно возрастают с большими коллекциями.
То же самое относится и к форме "единого документа":
db.collection.aggregate([
{ "$project": { "sales": "$sales" } },
{ "$unwind": "$sales" },
{ "$match": {
"sales.hidden": { "$ne": true }
}},
{ "$project": {
"sales": { "$objectToArray": "$sales.salesTotals" }
}},
{ "$unwind": "$sales" },
{ "$group": {
"_id": {
"_id": "$_id",
"k": "$sales.k"
},
"v": { "$sum": "$sales.v" }
}},
{ "$group": {
"_id": "$_id._id",
"data": { "$push": { "k": "$_id.k", "v": "$v" } }
}},
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": [
{ "_id": "$_id" },
{ "$arrayToObject": "$data" }
]
}
}}
])
Существует лишь небольшое изменение в этапах $group
в конце и, конечно, сохранение значения _id
документа в конечном результате приводит к восстановлению ключей.
Конечно, результаты, как и ожидалось, могут быть такими:
{
"baklava" : 20,
"pie" : 20,
"cake" : 20
}
Или для каждого документа (вы предоставили только один):
{
"_id" : "5bea815d2791a76283a2747a",
"cake" : 20,
"pie" : 20,
"baklava" : 20
}
Единственное, что, по крайней мере, показывают последние формы, это то, что с точки зрения обучения гораздо проще просто добавлять один этап конвейера за раз и видеть, как каждый этап влияет на результаты с изменениями, которые он фактически делает. .
Разобрать начальные формы может быть немного сложнее для понимания, но если вы потратите время на просмотр каждой части, вы в конечном итоге увидите, как они все сочетаются друг с другом.
Альтернативная картаReduce
Хотя вы не можете получить ту же производительность, что и структура агрегации, если у вас была MongoDB до поздней версии 3.4, вы всегда можете использовать mapReduce
:
db.collection.mapReduce(
function() {
this.sales.forEach(s => {
if (!s.hidden)
emit(null, s.salesTotals);
})
},
function(key,values) {
var obj = {};
values.forEach(value =>
Object.keys(value).forEach(k => {
if (!obj.hasOwnProperty(k))
obj[k] = 0;
obj[k] += value[k];
})
)
return obj;
},
{ out: { inline: 1 } }
)
Вывод немного отличается, поскольку mapReduce имеет строгую форму вывода «ключ / значение»:
{
"_id" : null,
"value" : {
"cake" : 20,
"pie" : 20,
"baklava" : 20
}
}
А для каждого документа достаточно просто заменить null
в emit()
текущим документом _id
значение:
db.collection.mapReduce(
function() {
var id = this._id;
this.sales.forEach(s => {
if (!s.hidden)
emit(id, s.salesTotals);
})
},
function(key,values) {
var obj = {};
values.forEach(value =>
Object.keys(value).forEach(k => {
if (!obj.hasOwnProperty(k))
obj[k] = 0;
obj[k] += value[k];
})
)
return obj;
},
{ out: { inline: 1 } }
)
С довольно очевидными результатами:
{
"_id" : "5bea815d2791a76283a2747a",
"value" : {
"cake" : 20,
"pie" : 20,
"baklava" : 20
}
}
Не так быстро, но довольно простой процесс, который снова использует Object.keys()
как способ извлечь произведение с "именованными ключами", не зная их имен.