#node.js #mongodb #aggregation-framework #grouping #pipeline
Вопрос:
Мне нужно изменить структуру некоторого поля в моем документе MongoDB.
Вот образец:
[
{
_id: "ObjectId('997v2ha1cv9b0036fa648zx3')",
title: "Adidas Predator",
size: "8",
colors: [
{
hex: "005FFF",
name: "Blue"
},
{
hex: "FF003A",
name: "Red"
},
{
hex: "FFFE00",
name: "Yellow"
},
{
hex: "07FF00",
name: "Green"
},
],
extras: [
{
description: "laces",
type: "exterior"
},
{
description: "sole",
type: "interior"
},
{
description: "logo"
},
{
description: "stud",
type: "exterior"
}
],
media: {
images: [
{
url: "http://link.com",
type: "exterior"
},
{
url: "http://link3.com",
type: "interior"
},
{
url: "http://link2.com",
type: "interior"
},
{
url: "http://link4.com",
type: "exterior"
}
]
}
}
];
Моя цель-сгруппировать некоторые поля:
цвета должны быть и массив только с цветами,
дополнительно должен быть массив с 3 объектами каждый для «типа» (внутренний, внешний, нулевой)
то же самое относится и к изображениям, находящимся внутри носителя
Вот чего я ожидал:
{
_id: "ObjectId('997b5aa1cv9b0036fa648ab5')",
title: "Adidas Predator",
size: "8",
colors: ["Blue", "Red", "Yellow", "Green"],
extras: [
{type: exterior, description: ["laces", "stud"]},
{type: interior, description: ["sole"]},
{type: null, description: ["logo"]}
],
images: [
{type: exterior, url: ["http://link.com", "http://link4.com"]},
{type: interior, url: ["http://link2.com", "http://link3.com"]},
]
};
С помощью моего кода я могу достичь своей цели, но я не понимаю, как показать всю информацию вместе через конвейер.
Вот мой код:
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
extras: 1,
media: "$media.images"
},
},
{
$unwind: "$media"
},
{
$group: {
_id: {
type: "$media.type",
url: "$media.url",
},
},
},
{
$group: {
_id: "$_id.type",
url: {
$push: "$_id.url"
},
},
},
]);
В результате получается:
[
{
_id: "exterior",
url: [
"http://link.com",
"http://link4.com"
]
},
{
_id: "interior",
url: [
"http://link3.com",
"http://link2.com"
]
}
];
Если я сделаю то же самое с дополнительными функциями, я получу ту же (правильную) структуру.
Как я могу показать все данные вместе, как в ожидаемой структуре?
Спасибо за совет.
Ответ №1:
Стратегия будет заключаться в том, чтобы поддерживать требуемые родительские поля по всему конвейеру, используя $first
только начальное значение, это не очень красиво, но это работает:
db.collection.aggregate([
{
"$addFields": {
colors: {
$map: {
input: "$colors",
as: "color",
in: "$color.name"
}
}
}
},
{
$unwind: "$extras"
},
{
"$addFields": {
imageUrls: {
$map: {
input: {
$filter: {
input: "$media.images",
as: "image",
cond: {
$eq: [
"$image.type",
"$extras.type"
]
}
}
},
as: "image",
in: "$image.url"
}
}
}
},
{
$group: {
_id: {
_id: "$_id",
extraType: "$extras.type"
},
extraDescriptions: {
"$addToSet": "$extras.description"
},
imageUrls: {
"$first": "$imageUrls"
},
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
}
}
},
{
$group: {
_id: "$_id._id",
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
},
images: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
url: "$imageUrls"
}
},
extras: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
description: "$extraDescriptions"
}
}
}
}
])
Комментарии:
1. Прежде всего, спасибо за ответ. Мне интересно, есть ли способ использовать вместо этого некоторую агрегацию этапов, например $project
2. Вы имеете в виду проект, а не группу?
3. Я имею в виду использовать больше этапов, таких как группа, вместо группировки с помощью карты, а затем использовать проект в конце конвейера, чтобы показать результат
4. Я предполагаю, что теоретически это возможно, используя
$reduce
и$filter
, но это было бы и кошмаром для написания, и намного, намного хуже с точки зрения эффективности, я не уверен, почему вы хотите пойти этим путем?
Ответ №2:
Вы можете попробовать оператор $function, чтобы определить пользовательскую функцию агрегации или выражение в JavaScript.
$project
чтобы отобразить необходимые поля и получить массивcolors
имени$function
, напишите свою логику JS, если вам нужно, вы можете отсортировать эту логику группы, она вернет результат с 2 полями (extras
,images
)$project
чтобы отобразить необходимые поля и отделитьextras
images
поле и от результата
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
result: {
$function: {
body: function(extras, images) {
function groupBy(objectArray, k, v) {
var results = [], res = objectArray.reduce((acc, obj) => {
if (!acc[obj[k]]) acc[obj[k]] = [];
acc[obj[k]].push(obj[v]);
return acc;
}, {});
for (var o in res) {
results.push({ [k]: o === 'undefined' ? null : o, [v]: res[o] })
}
return results;
}
return {
extras: groupBy(extras, 'type', 'description'),
images: groupBy(images, 'type', 'url')
}
},
args: ["$extras", "$media.images"],
lang: "js"
}
}
}
},
{
$project: {
title: 1,
size: 1,
colors: 1,
extras: "$result.extras",
images: "$result.images"
}
}
])
важный:
Выполнение JavaScript внутри выражения агрегации может снизить производительность. Используйте$function
оператора только в том случае, если предоставленные операторы конвейера не могут удовлетворить потребности вашего приложения.
Комментарии:
1. Спасибо. Я согласен с разделом «важно», который вы написали. Может быть, ту же операцию можно выполнить с помощью некоторых операторов mongo вместо js?
2. вот почему я добавил последний вариант. я не думаю, что это возможно сделать только с некоторыми операторами mongodb. я бы предположил, что вы также можете выполнять такого рода операции на языке программирования.