Det finns olika tillvägagångssätt beroende på vilken version som är tillgänglig, men de går i huvudsak upp till att omvandla dina dokumentfält till separata dokument i en "array", sedan "avveckla" den matrisen med $unwind
och gör successivt $group
steg för att ackumulera utdatasummor och matriser.
MongoDB 3.4.4 och senare
De senaste utgåvorna har speciella operatorer som $arrayToObject
och $objectToArray
vilket kan göra överföringen till den initiala "arrayen" från källdokumentet mer dynamisk än i tidigare utgåvor:
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"
}
}}
])
Så använder $objectToArray
du gör det initiala dokumentet till en uppsättning av dess nycklar och värden som "k"
och "v"
nycklar i den resulterande uppsättningen av objekt. Vi tillämpar $filter
här för att välja med "knapp". Här använder du $in
med en lista över nycklar som vi vill ha, men detta skulle kunna användas mer dynamiskt som en lista med nycklar för att "utesluta" där det var kortare. Det är bara att använda logiska operatorer för att utvärdera villkoret.
Slutsteget här använder $replaceRoot
och eftersom all vår manipulation och "gruppering" däremellan fortfarande behåller det "k"
och "v"
formulär använder vi sedan $arrayToObject
här för att marknadsföra vår "array av objekt" som resultat till "nycklarna" för dokumentet på högsta nivån i utdata.
MongoDB 3.6 $mergeObjects
Som en extra rynka här inkluderar MongoDB 3.6 $mergeObjects
som kan användas som en "ackumulator "
i en $group
pipeline-steget också, vilket ersätter $push
och gör den sista $replaceRoot
helt enkelt flytta "data"
nyckeln till "roten" av det returnerade dokumentet istället:
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" } }
])
Detta är egentligen inte så annorlunda mot vad som demonstreras överlag, utan visar helt enkelt hur $mergeObjects
kan användas på det här sättet och kan vara användbart i de fall där grupperingsnyckeln var något annorlunda och vi inte ville ha den slutliga "sammanslagningen" till objektets rotutrymme.
Observera att $arrayToObject
behövs fortfarande för att omvandla "värdet" tillbaka till namnet på "nyckeln", men vi gör det bara under ackumuleringen snarare än efter grupperingen, eftersom den nya ackumuleringen tillåter "sammanslagning" av nycklar.
MongoDB 3.2
Om du tar tillbaka en version eller till och med om du har en MongoDB 3.4.x som är mindre än 3.4.4-versionen, kan vi fortfarande använda mycket av detta men istället hanterar vi skapandet av arrayen på ett mer statiskt sätt också eftersom vi hanterar den slutliga "transformen" på utdata annorlunda på grund av de aggregeringsoperatorer som vi inte har:
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),{})
)
Detta är exakt samma sak, förutom att istället för att ha en dynamisk transformation av dokumentet till arrayen, tilldelar vi faktiskt varje arraymedlem samma "k"
"explicit" och "v"
notation. Behåller egentligen bara dessa nyckelnamn för konventionen vid det här laget eftersom ingen av aggregeringsoperatörerna här är beroende av det alls.
Också istället för att använda $replaceRoot
, vi gör precis samma sak som vad den tidigare implementeringen av pipelinestadiet gjorde där, men i klientkoden istället. Alla MongoDB-drivrutiner har någon implementering av cursor.map()
för att aktivera "marköromvandlingar". Här med skalet använder vi de grundläggande JavaScript-funktionerna i Array.map()
och Array.reduce()
för att ta den utmatningen och återigen främja arrayinnehållet till att vara nycklarna för dokumentet på högsta nivån.
MongoDB 2.6
Och faller tillbaka till MongoDB 2.6 för att täcka versionerna däremellan, det enda som ändras här är användningen av $map
och en $literal
för inmatning med matrisdeklarationen:
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),{})
)
Eftersom grundidén här är att "iterera" en tillhandahållen uppsättning av fältnamnen, kommer den faktiska tilldelningen av värden genom att "kapsla" $cond
uttalanden. För tre möjliga utfall betyder detta bara en enda kapsling för att "grena" för varje resultat.
Modern MongoDB från 3.4 har $switch
vilket gör denna förgrening enklare, men detta visar att logiken alltid var möjlig och $cond
operatören har funnits sedan aggregeringsramverket introducerades i MongoDB 2.2.
Återigen gäller samma transformation på markörresultatet eftersom det inte finns något nytt där och de flesta programmeringsspråk har förmågan att göra detta i flera år, om inte från början.
Naturligtvis kan den grundläggande processen till och med göras långt tillbaka till MongoDB 2.2, men bara genom att använda arrayskapandet och $unwind
på ett annat sätt. Men ingen borde köra någon MongoDB under 2.8 vid denna tidpunkt, och officiellt stöd även från 3.0 håller på att ta slut.
Utdata
För visualisering har utdata från alla demonstrerade pipelines här följande form innan den sista "transformen" görs:
/* 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
}
]
}
]
}
Och sedan antingen genom $replaceRoot
eller markörtransformeringen som visas blir resultatet:
/* 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
}
]
}
Så även om vi kan lägga in några nya och snygga operatörer i aggregeringspipelinen där vi har de tillgängliga, är det vanligaste användningsfallet i dessa "end of pipeline-transformeringar", i vilket fall vi lika gärna kan göra samma transformation på varje dokument i markörresultaten returnerades istället.