Existen diferentes enfoques dependiendo de la versión disponible, pero todos se dividen esencialmente para transformar los campos de su documento en documentos separados en una "matriz", luego "desenrollar" esa matriz con $unwind
y haciendo sucesivos $group
etapas para acumular los totales de salida y las matrices.
MongoDB 3.4.4 y superior
Las últimas versiones tienen operadores especiales como $arrayToObject
y $objectToArray
lo que puede hacer que la transferencia a la "matriz" inicial desde el documento de origen sea más dinámica que en versiones anteriores:
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"
}
}}
])
Entonces usando $objectToArray
conviertes el documento inicial en una matriz de sus claves y valores como "k"
y "v"
claves en la matriz resultante de objetos. Aplicamos $filter
aquí para seleccionar por "clave". Aquí usando $in
con una lista de claves que queremos, pero esto podría usarse de manera más dinámica como una lista de claves para "excluir" donde era más corto. Solo usa operadores lógicos para evaluar la condición.
La etapa final aquí usa $replaceRoot
y dado que toda nuestra manipulación y "agrupación" en el medio aún mantiene ese "k"
y "v"
formulario, luego usamos $arrayToObject
aquí para promocionar nuestra "matriz de objetos" como resultado a las "claves" del documento de nivel superior en la salida.
MongoDB 3.6 $mergeObjects
Como detalle adicional aquí, MongoDB 3.6 incluye $mergeObjects
que se puede usar como un "acumulador "
en un $group
etapa de canalización también, reemplazando así el $push
y haciendo el final $replaceRoot
simplemente cambiando los "data"
clave para la "raíz" del documento devuelto en su lugar:
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" } }
])
Esto no es tan diferente de lo que se demuestra en general, sino que simplemente demuestra cómo $mergeObjects
se puede usar de esta manera y puede ser útil en casos en los que la clave de agrupación era algo diferente y no queríamos esa "fusión" final con el espacio raíz del objeto.
Tenga en cuenta que $arrayToObject
Todavía se necesita transformar el "valor" nuevamente en el nombre de la "clave", pero lo hacemos durante la acumulación en lugar de después de la agrupación, ya que la nueva acumulación permite la "fusión" de claves.
MongoDB 3.2
Retomando una versión o incluso si tiene un MongoDB 3.4.x que es anterior a la versión 3.4.4, aún podemos usar gran parte de esto, pero en su lugar, también nos ocupamos de la creación de la matriz de una manera más estática. como manejar la "transformación" final en la salida de manera diferente debido a los operadores de agregación que no tenemos:
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),{})
)
Esto es exactamente lo mismo, excepto que en lugar de tener una transformación dinámica del documento en la matriz, en realidad asignamos "explícitamente" a cada miembro de la matriz con el mismo "k"
y "v"
notación. Realmente solo mantengo esos nombres clave por convención en este punto, ya que ninguno de los operadores de agregación aquí depende de eso en absoluto.
También en lugar de usar $replaceRoot
, hacemos exactamente lo mismo que la implementación de la etapa de canalización anterior, pero en el código del cliente. Todos los controladores MongoDB tienen alguna implementación de cursor.map()
para habilitar las "transformaciones del cursor". Aquí, con el shell, usamos las funciones básicas de JavaScript de Array.map()
y Array.reduce()
para tomar esa salida y nuevamente promover el contenido de la matriz para que sean las claves del documento de nivel superior devuelto.
MongoDB 2.6
Y volviendo a MongoDB 2.6 para cubrir las versiones intermedias, lo único que cambia aquí es el uso de $map
y un $literal
para la entrada con la declaración de matriz:
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),{})
)
Dado que la idea básica aquí es "iterar" una matriz proporcionada de los nombres de los campos, la asignación real de valores se produce al "anidar" el $cond
declaraciones. Para tres resultados posibles, esto significa solo una sola anidación para "ramificarse" para cada resultado.
MongoDB moderno de 3.4 tiene $switch
lo que simplifica esta bifurcación, pero esto demuestra que la lógica siempre fue posible y $cond
El operador existe desde que se introdujo el marco de agregación en MongoDB 2.2.
Nuevamente, se aplica la misma transformación en el resultado del cursor ya que no hay nada nuevo allí y la mayoría de los lenguajes de programación tienen la capacidad de hacer esto durante años, si no desde el inicio.
Por supuesto, el proceso básico incluso se puede realizar desde MongoDB 2.2, pero simplemente aplicando la creación de matriz y $unwind
de una manera diferente. Pero nadie debería estar ejecutando ningún MongoDB bajo 2.8 en este momento, y el soporte oficial incluso desde 3.0 se está agotando rápidamente.
Salida
Para la visualización, la salida de todas las canalizaciones demostradas aquí tiene el siguiente formato antes de que se realice la última "transformación":
/* 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
}
]
}
]
}
Y luego por $replaceRoot
o la transformación del cursor como se demuestra, el resultado se convierte en:
/* 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
}
]
}
Entonces, si bien podemos poner algunos operadores nuevos y sofisticados en la canalización de agregación donde los tenemos disponibles, el caso de uso más común es en estas "transformaciones de final de canalización", en cuyo caso también podemos simplemente hacer la misma transformación en cada documento en en su lugar, se devolvieron los resultados del cursor.