Como descargar archivos CSV de miles de datos con MongoDB/NodeJs y no morir en el intento


Hola Dev’s recientemente me encontraba desarrollando una plataforma con React.js, node.js y mongodb para mostrar datos sobre delitos en Mexico.

En una de las secciones tiene la opción de descargar los datos en formato CSV o JSON, sin embargo son aproximadamente unos 4 a 6 millones de datos de la colección que contiene la información.

El problema

Nos encontramos con el problema que al ejecutar una consulta con el cursor o toArray para generar la data y construir el archivo CSV no era buena opción, ya que el servidor se quedaba sin memoria o se desconectaba al filtrar y ordenar los datos.

Solución

Entonces utilizamos el stream de mongodb para generar el archivo mientras va leyendo cada registro en la base de datos.

Podemos usar express con Sails.js ó Next.js, voy asumir que estamos familiarizados con estas herramientas y de como funciona el router y solicitudes HTTP.

Debemos importar en nuestro código mongoclient como se muestra a continuación:

var MongoClient = require('mongodb').MongoClient;


module.exports = {
    donwloadCSV : (req,res) => {
        
    }
}

Bien! ahora debemos conectarnos a mongo y realizar nuestra consulta a la base de datos y la colección, utilizamos el siguiente código

var MongoClient = require('mongodb').MongoClient;


module.exports = {
    donwloadCSV : (req,res) => {
        MongoClient.connect('mongodb://myiporhost:myport/mydatabase',(err, db) => {
                if (err)
                    return console.warn(err);
                
                const stream = db
                    .collection('municipality')
                    .find(query)
                    .project({
                        name : 1,
                        description : 1,
                        total : 1
                    })
                    .stream({ transform: streamTransformer });
            }
        );    
    }
}

Es muy importante solo pedir los datos que vamos a utilizar por eso usamos la función project indicando los que campos que debe regresar.

Ahora cada vez que encuentre un registro que coincida con nuestro query se ejecutara la función streamTransformer donde vamos a convertir nuestro JSON a un String separado por comas para ir creando el archivo CSV

var MongoClient = require('mongodb').MongoClient;

function streamTransformer(doc) {
    var newObject = {
        nombre : doc.name,
        decripcion : doc.description,
        date: moment(doc.date).format('YYYY-MM'),
    };
    return `${Object.values(newObject).join(',')}\n`;
}

module.exports = {
    donwloadCSV : (req,res) => {
        MongoClient.connect('mongodb://myiporhost:myport/mydatabase',(err, db) => {
                if (err)
                    return console.warn(err);

                const stream = db
                    .collection('municipality')
                    .find(query)
                    .project({
                        name : 1,
                        description : 1,
                        total : 1
                    })
                    .stream({ transform: streamTransformer });
            }
        );    
    }
}

Cada vez que se ejecute la función retornara un String con el nombre, descripción, fecha y un salto de linea aquí vamos ir construyendo linea a linea nuestro archivo CSV

ya solo nos falta responder el archivo CSV para que se genere el stream y empiece a construir el archivo mientras va leyendo los datos esto nos ahorra mucha memoria RAM y recursos ya que solo se genera la información conforme va llegando

var MongoClient = require('mongodb').MongoClient;

function streamTransformer(doc) {
    var newObject = {
        nombre : doc.name,
        decripcion : doc.description,
        date: moment(doc.date).format('YYYY-MM'),
    };
    return `${Object.values(newObject).join(',')}\n`;
}

module.exports = {
    donwloadCSV : (req,res) => {
        MongoClient.connect('mongodb://myiporhost:myport/mydatabase',(err, db) => {
                if (err)
                    return console.warn(err);

                const stream = db
                    .collection('municipality')
                    .find(query)
                    .project({
                        name : 1,
                        description : 1,
                        total : 1
                    })
                    .stream({ transform: streamTransformer });

                stream.once('end', function() {
                    db.close();
                });

                res.setHeader('content-type', 'text/csv;charset=utf-8');
                res.setHeader(
                    'content-disposition',
                    'attachment; filename=datos.csv'
                );

                res.write('nombre,descripcion,fecha\n');
                return stream.pipe(res);
            }
        );    
    }
}

Como puedes observar añadimos el formato UTF8, el nombre del archivo que va a guardar en el navegador cuando se descargue, también añadimos las cabeceras del archivo CSV para indicar como se llama cada columna.

Y una vez que finalice el stream cerramos la conexión con la base de datos.

En cuanto a redimiendo logramos pasar de un 70% de uso de CPU y 1GB de memoria RAM que utilizaba al crear el CSV a solo 2% de CPU y 100MB de RAM, logrando exportar archivos con 50 mil, 200 mil y medio millón de datos.

Gracias por leer y compartir este post, si deseas puedes hacer un donativo de 1 dolar para seguir manteniendo y subir más contenido en este sitio.