Има различни подходи в зависимост от наличната версия, но всички те по същество се свеждат до трансформиране на вашите документни полета в отделни документи в „масив“, след което „отвиване“ на този масив с $unwind
и извършване на последователни $group
етапи, за да се натрупат изходните суми и масиви.
MongoDB 3.4.4 и по-нова версия
Последните версии имат специални оператори като $arrayToObject
и $objectToArray
което може да направи прехвърлянето към първоначалния "масив" от изходния документ по-динамично, отколкото в по-ранните версии:
db.profile.aggregate([
{ "$project": {
"_id": 0,
"data": {
"$filter": {
"input": { "$objectToArray": "$$ROOT" },
"cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
}
}
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": "$data",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.k",
"v": {
"$push": { "name": "$_id.v", "total": "$total" }
}
}},
{ "$group": {
"_id": null,
"data": { "$push": { "k": "$_id", "v": "$v" } }
}},
{ "$replaceRoot": {
"newRoot": {
"$arrayToObject": "$data"
}
}}
])
Така че използвайки $objectToArray
правите първоначалния документ в масив от неговите ключове и стойности като "k"
и "v"
ключове в резултантния масив от обекти. Прилагаме $filter
тук, за да изберете по "ключ". Тук се използва $in
със списък от ключове, които искаме, но това може да се използва по-динамично като списък от ключове за „изключване“, където това е по-кратко. Това е просто използване на логически оператори за оценка на условието.
Крайният етап тук използва $replaceRoot
и тъй като цялата ни манипулация и "групиране" между тях все още запазва това "k"
и "v"
след това използваме $arrayToObject
тук, за да популяризираме нашия „масив от обекти“ в резултат към „ключовете“ на документа от най-високо ниво в изхода.
MongoDB 3.6 $mergeObjects
Като допълнителна бръчка тук, MongoDB 3.6 включва $mergeObjects
който може да се използва като "акумулатор "
в $group
етап на тръбопровод, като по този начин замества $push
и прави окончателния $replaceRoot
просто преместване на "данни"
вместо това въведете „корена“ на върнатия документ:
db.profile.aggregate([
{ "$project": {
"_id": 0,
"data": {
"$filter": {
"input": { "$objectToArray": "$$ROOT" },
"cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
}
}
}},
{ "$unwind": "$data" },
{ "$group": { "_id": "$data", "total": { "$sum": 1 } }},
{ "$group": {
"_id": "$_id.k",
"v": {
"$push": { "name": "$_id.v", "total": "$total" }
}
}},
{ "$group": {
"_id": null,
"data": {
"$mergeObjects": {
"$arrayToObject": [
[{ "k": "$_id", "v": "$v" }]
]
}
}
}},
{ "$replaceRoot": { "newRoot": "$data" } }
])
Това всъщност не е толкова различно от това, което се демонстрира като цяло, а просто демонстрира как $mergeObjects
може да се използва по този начин и може да бъде полезен в случаите, когато ключът за групиране е нещо различно и ние не искаме това окончателно „сливане“ към основното пространство на обекта.
Имайте предвид, че $arrayToObject
все още е необходимо за преобразуване на „стойността“ обратно в името на „ключа“, но ние просто го правим по време на натрупването, а не след групирането, тъй като новото натрупване позволява „сливане“ на ключове.
MongoDB 3.2
Вземайки го обратно версия или дори ако имате MongoDB 3.4.x, която е по-малка от версията 3.4.4, все още можем да използваме голяма част от това, но вместо това се занимаваме със създаването на масива също по по-статичен начин като обработка на крайната "трансформация" на изхода по различен начин поради операторите за агрегиране, които нямаме:
db.profile.aggregate([
{ "$project": {
"data": [
{ "k": "gender", "v": "$gender" },
{ "k": "caste", "v": "$caste" },
{ "k": "education", "v": "$education" }
]
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": "$data",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.k",
"v": {
"$push": { "name": "$_id.v", "total": "$total" }
}
}},
{ "$group": {
"_id": null,
"data": { "$push": { "k": "$_id", "v": "$v" } }
}},
/*
{ "$replaceRoot": {
"newRoot": {
"$arrayToObject": "$data"
}
}}
*/
]).map( d =>
d.data.map( e => ({ [e.k]: e.v }) )
.reduce((acc,curr) => Object.assign(acc,curr),{})
)
Това е абсолютно същото, но вместо да имаме динамична трансформация на документа в масива, ние всъщност "изрично" присвояваме на всеки член на масива един и същ "k"
и "v"
нотация. Наистина просто запазвам тези ключови имена за конвенция в този момент, тъй като нито един от операторите за агрегиране тук изобщо не зависи от това.
Също така вместо да използвате $replaceRoot
, ние просто правим абсолютно същото като това, което правеше там изпълнението на предишния етап на тръбопровода, но вместо това в клиентския код. Всички драйвери на MongoDB имат някаква реализация на cursor.map()код>
за да разрешите "трансформиране на курсора". Тук с обвивката използваме основните функции на JavaScript на Array.map()
и Array.reduce()
за да вземе този изход и отново да повиши съдържанието на масива като ключове на върнатия документ от най-високо ниво.
MongoDB 2.6
И връщайки се към MongoDB 2.6, за да покрием версиите между тях, единственото нещо, което се променя тук, е използването на $map
и $literal
за въвеждане с декларацията на масива:
db.profile.aggregate([
{ "$project": {
"data": {
"$map": {
"input": { "$literal": ["gender","caste", "education"] },
"as": "k",
"in": {
"k": "$$k",
"v": {
"$cond": {
"if": { "$eq": [ "$$k", "gender" ] },
"then": "$gender",
"else": {
"$cond": {
"if": { "$eq": [ "$$k", "caste" ] },
"then": "$caste",
"else": "$education"
}
}
}
}
}
}
}
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": "$data",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.k",
"v": {
"$push": { "name": "$_id.v", "total": "$total" }
}
}},
{ "$group": {
"_id": null,
"data": { "$push": { "k": "$_id", "v": "$v" } }
}},
/*
{ "$replaceRoot": {
"newRoot": {
"$arrayToObject": "$data"
}
}}
*/
])
.map( d =>
d.data.map( e => ({ [e.k]: e.v }) )
.reduce((acc,curr) => Object.assign(acc,curr),{})
)
Тъй като основната идея тук е да се „итерира“ предоставен масив от имена на полета, действителното присвояване на стойности идва чрез „влагане“ на $cond
изявления. За три възможни резултата това означава само едно влагане, за да се „разклони“ за всеки резултат.
Съвременната MongoDB от 3.4 има $switch
което прави това разклоняване по-просто, но това демонстрира, че логиката винаги е била възможна и $cond
оператор съществува, откакто рамката за агрегиране беше въведена в MongoDB 2.2.
Отново се прилага същата трансформация на резултата от курсора, тъй като там няма нищо ново и повечето езици за програмиране имат способността да правят това в продължение на години, ако не и от самото начало.
Разбира се, основният процес може дори да се извърши обратно към MongoDB 2.2, но само като се приложи създаването на масив и $unwind
по различен начин. Но никой не трябва да работи с MongoDB под 2.8 в този момент, а официалната поддръжка дори от 3.0 бързо се изчерпва.
Изход
За визуализация изходът на всички демонстрирани тръбопроводи тук има следната форма, преди да бъде направена последната "трансформация":
/* 1 */
{
"_id" : null,
"data" : [
{
"k" : "gender",
"v" : [
{
"name" : "Male",
"total" : 3.0
},
{
"name" : "Female",
"total" : 2.0
}
]
},
{
"k" : "education",
"v" : [
{
"name" : "M.C.A",
"total" : 1.0
},
{
"name" : "B.E",
"total" : 3.0
},
{
"name" : "B.Com",
"total" : 1.0
}
]
},
{
"k" : "caste",
"v" : [
{
"name" : "Lingayath",
"total" : 3.0
},
{
"name" : "Vokkaliga",
"total" : 2.0
}
]
}
]
}
И след това или чрез $replaceRoot
или трансформацията на курсора, както е показано, резултатът става:
/* 1 */
{
"gender" : [
{
"name" : "Male",
"total" : 3.0
},
{
"name" : "Female",
"total" : 2.0
}
],
"education" : [
{
"name" : "M.C.A",
"total" : 1.0
},
{
"name" : "B.E",
"total" : 3.0
},
{
"name" : "B.Com",
"total" : 1.0
}
],
"caste" : [
{
"name" : "Lingayath",
"total" : 3.0
},
{
"name" : "Vokkaliga",
"total" : 2.0
}
]
}
Така че докато можем да поставим някои нови и фантастични оператори в тръбопровода за агрегиране, където имаме такива, най-честият случай на употреба е в тези „трансформации в края на конвейера“, в който случай можем просто да направим същата трансформация на всеки документ в резултатите от курсора се върнаха вместо това.