En un anterior artículo hablamos sobre cómo consultar los documentos repetidos introduciéndonos en el mundo de MongoDB. Ahora aprenderemos a cómo ordenar un array que está dentro de un documento sobre una consulta de varios documentos, ampliando así nuestro juego de consultas. El resultado final se espera que sea como una consulta estándar, es decir, misma estructura y campos (db.products.find({})) a excepción de filtrados y ordenación.

Utilizaremos el “aggregation pipeline” para ir añadiendo varios “stages” hasta llegar al resultado esperado.

Caso

Tenemos una colección con varios productos relativos a diferentes aparatos electrónicos/informáticos en donde algunos productos tienen unas etiquetas ó “tags” para ser clasificados. Los «tags», es un array que está formado por objetos con un campo llamado “name” y por otro llamado “order” que será por el cual queremos ordenar dichas etiquetas.

db.products.insertMany([
   {
      name: "Mouse",
      code: "08001",
      colour: "Black",
      price: 13,
      tags: [ 
        {
            order: 2,
            name: "optical"
        }, 
        {
            order: 1,
            name: "wireless"
        }
      ]
   },
   {
      name: "Monitor",
      code: "08002",
      colour: "White",
      price: 20,
      tags: [ 
        {
           order: 1,
           name: "led"
        }, 
        {
           order: 2,
           name: "colour"
        }, 
        {
           order: 4,
           name: "gaming"
        }, 
        {
           order: 3,
           name: "speakers"
        }
     ]
   },
   {
      name: "HardDisk",
      code: "08003",
      colour: "Silver",
      price: 100,
      tags : []
   },
   {
      name: "PenDrive",
      code: "08001",
      colour: "Gray",
      price: 5
   },
   {
      name: "Tablet",
      code: "08002",
      colour: "Blue",
      price: 50
   },
   {
      name: "SmartPhone",
      code: "08002",
      colour: "Black",
      price: 80
   }
])

Como podemos observar, el producto “Mouse” tiene dos tags, el “Monitor” 4 tags, “HardDisk” un array de tags vacíos y el resto de productos no tienen tags. Y lo que se desea es que al listar los productos “products”, el array de tags de cada producto salga ordenados por el campo “order” del «tag».

Stage 1: Unwind

Para poder ordenar por el array, primero debemos realizar un «unwind» para tener un documento para cada tag que tiene el array sin perder los documentos que no tienen tags o su array esté vacío mediante la propiedad “preserveNullAndEmptyArrays”.

Este stage sería como:

{ 
    $unwind: { path: "$tags", preserveNullAndEmptyArrays: true }
}

Resultados:

/* 1 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : {
        "order" : 2,
        "name" : "secondary"
    }
}

/* 2 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : {
        "order" : 1,
        "name" : "main"
    }
}

/* 3 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 1,
        "name" : "typescript"
    }
}

/* 4 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 2,
        "name" : "angular"
    }
}

/* 5 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 4,
        "name" : "css"
    }
}

/* 6 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 3,
        "name" : "html"
    }
}

/* 7 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
    "name" : "HardDisk",
    "code" : "08003",
    "colour" : "Silver",
    "price" : 100.0
}

/* 8 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
    "name" : "PenDrive",
    "code" : "08001",
    "colour" : "Gray",
    "price" : 5.0
}

/* 9 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
    "name" : "Tablet",
    "code" : "08002",
    "colour" : "Blue",
    "price" : 50.0
}

/* 10 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
    "name" : "SmartPhone",
    "code" : "08002",
    "colour" : "Black",
    "price" : 80.0
}

Stage 2: Ordenación

Con el aggregation “sort”, ahora podremos ordenar los documentos por el campo “order” del tag de la siguiente manera:

{ 
     $sort: { 'tags.order': 1 }
}

El número de elementos son los mismos pero esta vez están ordenados ascendentemente. Al principio tendremos los “products” que no tienen tags seguidos por los que sí tienen y ordenados por el criterio dado.

Resultados:

/* 1 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
    "name" : "HardDisk",
    "code" : "08003",
    "colour" : "Silver",
    "price" : 100.0
}

/* 2 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
    "name" : "PenDrive",
    "code" : "08001",
    "colour" : "Gray",
    "price" : 5.0
}

/* 3 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
    "name" : "Tablet",
    "code" : "08002",
    "colour" : "Blue",
    "price" : 50.0
}

/* 4 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
    "name" : "SmartPhone",
    "code" : "08002",
    "colour" : "Black",
    "price" : 80.0
}

/* 5 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : {
        "order" : 1,
        "name" : "main"
    }
}

/* 6 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 1,
        "name" : "typescript"
    }
}

/* 7 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : {
        "order" : 2,
        "name" : "secondary"
    }
}

/* 8 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 2,
        "name" : "angular"
    }
}

/* 9 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 3,
        "name" : "html"
    }
}

/* 10 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : {
        "order" : 4,
        "name" : "css"
    }
}

Stage 3: Agrupación

Aquí lo que se pretende es volver a unir el unwind del primer stage pero ahora al “juntar” de nuevo los tags estos estarán ordenados. No obstante, se obtendrá toda la información del documento original en un campo llamado “root” y a la vez añadiremos en un campo “tags” los tags que están ordenados.

{ 
     $group: { 
         _id: "$_id",
         root: { $mergeObjects: '$$ROOT' },   
         tags: { $push: "$tags" } 
     } 
}

Resultados:

/* 1 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
        "name" : "SmartPhone",
        "code" : "08002",
        "colour" : "Black",
        "price" : 80.0
    },
    "tags" : []
}

/* 2 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
        "name" : "Mouse",
        "code" : "08001",
        "colour" : "Black",
        "price" : 13.0,
        "tags" : {
            "order" : 2,
            "name" : "secondary"
        }
    },
    "tags" : [ 
        {
            "order" : 1,
            "name" : "main"
        }, 
        {
            "order" : 2,
            "name" : "secondary"
        }
    ]
}

/* 3 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
        "name" : "HardDisk",
        "code" : "08003",
        "colour" : "Silver",
        "price" : 100.0
    },
    "tags" : []
}

/* 4 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
        "name" : "Monitor",
        "code" : "08002",
        "colour" : "White",
        "price" : 20.0,
        "tags" : {
            "order" : 4,
            "name" : "css"
        }
    },
    "tags" : [ 
        {
            "order" : 1,
            "name" : "typescript"
        }, 
        {
            "order" : 2,
            "name" : "angular"
        }, 
        {
            "order" : 3,
            "name" : "html"
        }, 
        {
            "order" : 4,
            "name" : "css"
        }
    ]
}

/* 5 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
        "name" : "PenDrive",
        "code" : "08001",
        "colour" : "Gray",
        "price" : 5.0
    },
    "tags" : []
}

/* 6 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
        "name" : "Tablet",
        "code" : "08002",
        "colour" : "Blue",
        "price" : 50.0
    },
    "tags" : []
}

Stage 4: Formateo del documento principal (root)

Toca hacer ahora un merge del campo “root” con los “tags” ordenados de manera que ahora en el campo “root” está el documento con los “tags” ordenados como queremos:

{
    $replaceRoot: {
        newRoot: {
            $mergeObjects: ['$root', '$$ROOT']
        }
    }
}

Resultados:

/* 1 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
    "name" : "SmartPhone",
    "code" : "08002",
    "colour" : "Black",
    "price" : 80.0,
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
        "name" : "SmartPhone",
        "code" : "08002",
        "colour" : "Black",
        "price" : 80.0
    },
    "tags" : []
}

/* 2 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : [ 
        {
            "order" : 1,
            "name" : "main"
        }, 
        {
            "order" : 2,
            "name" : "secondary"
        }
    ],
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
        "name" : "Mouse",
        "code" : "08001",
        "colour" : "Black",
        "price" : 13.0,
        "tags" : {
            "order" : 2,
            "name" : "secondary"
        }
    }
}

/* 3 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
    "name" : "HardDisk",
    "code" : "08003",
    "colour" : "Silver",
    "price" : 100.0,
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
        "name" : "HardDisk",
        "code" : "08003",
        "colour" : "Silver",
        "price" : 100.0
    },
    "tags" : []
}

/* 4 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : [ 
        {
            "order" : 1,
            "name" : "typescript"
        }, 
        {
            "order" : 2,
            "name" : "angular"
        }, 
        {
            "order" : 3,
            "name" : "html"
        }, 
        {
            "order" : 4,
            "name" : "css"
        }
    ],
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
        "name" : "Monitor",
        "code" : "08002",
        "colour" : "White",
        "price" : 20.0,
        "tags" : {
            "order" : 4,
            "name" : "css"
        }
    }
}

/* 5 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
    "name" : "PenDrive",
    "code" : "08001",
    "colour" : "Gray",
    "price" : 5.0,
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
        "name" : "PenDrive",
        "code" : "08001",
        "colour" : "Gray",
        "price" : 5.0
    },
    "tags" : []
}

/* 6 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
    "name" : "Tablet",
    "code" : "08002",
    "colour" : "Blue",
    "price" : 50.0,
    "root" : {
        "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
        "name" : "Tablet",
        "code" : "08002",
        "colour" : "Blue",
        "price" : 50.0
    },
    "tags" : []
}

Stage 5: Seleccionar/Visualizar

En este último stage solo nos interesa poder ver el contenido como si una consulta normal fuera, es decir, en este caso sin el campo root, así que el stage quedaría:

{
    $project: {
        root: 0 
    }
}

Resultados:

/* 1 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190ca0"),
    "name" : "SmartPhone",
    "code" : "08002",
    "colour" : "Black",
    "price" : 80.0,
    "tags" : []
}

/* 2 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9b"),
    "name" : "Mouse",
    "code" : "08001",
    "colour" : "Black",
    "price" : 13.0,
    "tags" : [ 
        {
            "order" : 1,
            "name" : "main"
        }, 
        {
            "order" : 2,
            "name" : "secondary"
        }
    ]
}

/* 3 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9d"),
    "name" : "HardDisk",
    "code" : "08003",
    "colour" : "Silver",
    "price" : 100.0,
    "tags" : []
}

/* 4 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9c"),
    "name" : "Monitor",
    "code" : "08002",
    "colour" : "White",
    "price" : 20.0,
    "tags" : [ 
        {
            "order" : 1,
            "name" : "typescript"
        }, 
        {
            "order" : 2,
            "name" : "angular"
        }, 
        {
            "order" : 3,
            "name" : "html"
        }, 
        {
            "order" : 4,
            "name" : "css"
        }
    ]
}

/* 5 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9e"),
    "name" : "PenDrive",
    "code" : "08001",
    "colour" : "Gray",
    "price" : 5.0,
    "tags" : []
}

/* 6 */
{
    "_id" : ObjectId("5fdf766f2248ecb270190c9f"),
    "name" : "Tablet",
    "code" : "08002",
    "colour" : "Blue",
    "price" : 50.0,
    "tags" : []
}

Consulta final

Como consecuencia, uniendo los stages la consulta final quedaría de la siguiente forma:

db.products.aggregate([
{ 
    $unwind: { path: "$tags", preserveNullAndEmptyArrays: true }
},
{ 
    $sort: { 'tags.order': 1 }
},
{ 
    $group : { 
        _id : "$_id",
        root: { $mergeObjects: '$$ROOT' },   
        tags: { $push: "$tags" } 
    } 
},
{
     $replaceRoot: {
         newRoot: {
             $mergeObjects: ['$root', '$$ROOT']
         }
     }
},
{
    $project: {
        root: 0 
    }
}
])

Esa sería la consulta principal de obtener los documentos con sus tags ordenados por el campo “order”. Se han dejado fuera stages como filtrados (filtrado inicial o final, …) , ordenaciones (del resultado final) o paginaciones (skip, take), etc.

Conclusiones

Como en el anterior artículo sobre MongoDB, espero haber solucionado quizás alguna problemática así como la ampliación del repertorio de consultas.

Qué te ha parecido? Modificarías alguna cosa? Danos tú opinión!