Seleccionar página

MongoDB es sin duda una de las bases de datos NoSQL más populares y NodeJS es uno de los entornos desde el que más habitualmente se acceder a esta base de datos. La pila MEAN (Mongodb, Express, Angular y Node) ha popularizado este modelo, aunque realmente el uso de Express o Angular no son siempre los framework elegidos.

La utilización del driver de MongoDB se ha realizado fundamentalmente utilizando los callbacks como mecanismo de obtener el resultado de la ejecución asíncrona de cada una de las instrucciones sobre la base de datos, pero desde hace ya tiempo está disponible en el driver nativo la utilización de promesas, lo cual simplifica de forma muy significativa el uso de este API.

Si unimos estas promesas con las funciones generadoras y la popular librería Co, tenemos como resultado una forma mucho más sencilla y comprensible de utilizar el driver de MongoDB desde Node gracias a las nuevas funcionalidades de Ecmascript 6. Vamos a ver cómo.

Aunque sea bastante evidente, para vamos a necesitar:

  • Node 4.x o superior, ya que es a partir de estas versiones las que se va a soportar de forma nativa los generadores. En versiones anteriores ya se dio soporte a las promesas, pero es a partir de la versión 4 desde la que se da soporte nativo a los generadores.
  • MongoDB, en cualquiera de sus versiones, aunque en general se puede recomendar utilizar la versión 3.2.x ya que tiene algunas interesantes funcionalidades que podemos aprovechar en nuestros desarrollos, aunque para lo que aquí vamos a explicar realmente podemos utilizar cualquier versión.
  • Driver de MongoDB para NodeJS 2.x, aunque aún mejor si es la 2.1.x ya que es la que nos va a dar soporte para MongoDB 3.2 y es la versión estable del driver. Aunque hay bastantes alternativas para acceder a MongoDB desde NodeJS, en nuestro caso vamos a utilizar el driver nativo soportado por MongoDB. Este driver fue desarrollado inicialmente por Christian Kvalheim, quien se unió al equipo de MongoDB y desde 2012 es el driver oficial.
npm install mongodb --save
  • Co v4, sin duda la más popular librería para la gestión de llamadas a funciones generadoras. Aunque pueda parecer que Co hace algo raro o misterioso, lo cierto es que su código es bastante sencillo de leer y básicamente se encarga de llamar a todas las instrucciones yied de forma que el generador valla ejecutando todas sus instrucciones de forma consecutiva.
npm install co --save

Si quieres profundizar sobre el uso de callbacks, promesas y generadores, re recomendamos que leas el interesante artículo de Javier Vélez Reyes Programación asíncrona: paso de continuadores, eventos, promesas y generadores.

Conexión

Vamos a empezar con la conexión a MongoDB. Normalmente utilizaríamos una llamada más o menos de este tipo:

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

var url = 'mongodb://localhost:27017/test';

MongoClient.connect(url, function(err, db) {
    assert.equal(null, err);
    console.log("Connected correctly to server");
    db.close();
});

Con generadores y Co podemos utilizar un modelo más sencillo, evitando tener que utilizar la llamada al callback, aunque tenemos que incluir nuestra llamada dentro de una función generadora dentro de co().

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

co(function* () {
    var url = 'mongodb://localhost:27017/test';

    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");
    db.close();

}).catch(function (err) {
    console.error(err);
});

Vamos a ver paso a paso lo que hemos hecho: primero hemos incluido co como una librería más y hemos creado una sencilla llamada a co dentro de la cual hemos incluido una función generadora:

var co = require('co');
co(function* () {

});

A continuación hemos llamado a la conexión con la instrucción yield. Esta instrucción sólo puede encontrarse dentro de una función generadora (más adelante veremos que no podemos usarla sin más dentro de funciones invocadas desde forEach, map o métodos similares). Esta instrucción interrumpe la ejecución de la función generadora.

La función generadora la hemos incluido dentro de una llamada a co() que es quien se encargará de llamar a la función next() por cada uno de los yield que aparezcan en el código.

Por último, el resultado que habitualmente hubiera sido pasado como parámetro a la función callback, se inserta dentro de la variable db y el código sigue su ejecución sin mayor problema.

co() devuelve una promesa, por lo que el posible error en la conexión se gestionará en la función que pasaremos al catch().

Es posible que no consideremos mucho más sencilla la segunda versión con generadores y co(), que la primera versión con callback, pero, como veremos de aquí en adelante, cuando hay que anidar varias de estas llamadas la simplicidad del segundo método es evidente.

Añadir documentos

Para añadir documentos utilizaremos los métodos habituales del driver de MongoDB (insert(), insertOne() o insertMany()), recibiendo como valor de retorno el objeto que hubiéramos recibido en el callback.

En este ejemplo vamos a insertar los datos de las provincias españolas con su población de hombres y mujeres:

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

var poblacion = [
    {"provincia": "Albacete",               "hombres": 197014,  "mujeres": 197566  },
    {"provincia": "Alicante/Alacant",       "hombres": 920920,  "mujeres": 934127  },
    {"provincia": "Almería",                "hombres": 356058,  "mujeres": 345153  },
    {"provincia": "Asturias",               "hombres": 502175,  "mujeres": 549054  },
    {"provincia": "Araba/Álava",            "hombres": 160294,  "mujeres": 163354  },
    {"provincia": "Ávila",                  "hombres": 82880,   "mujeres": 82045   },
    {"provincia": "Badajoz",                "hombres": 340376,  "mujeres": 346354  },
    {"provincia": "Balears, Illes",         "hombres": 549678,  "mujeres": 554801  },
    {"provincia": "Barcelona",              "hombres": 2696360, "mujeres": 2827562 },
    {"provincia": "Bizkaia",                "hombres": 554832,  "mujeres": 593943  },
    {"provincia": "Burgos",                 "hombres": 182142,  "mujeres": 181860  },
    {"provincia": "Cáceres",                "hombres": 201702,  "mujeres": 204565  },
    {"provincia": "Cádiz",                  "hombres": 613094,  "mujeres": 627190  },
    {"provincia": "Cantabria",              "hombres": 284788,  "mujeres": 300391  },
    {"provincia": "Castellón/Castelló",     "hombres": 289720,  "mujeres": 292607  },
    {"provincia": "Ciudad Real",            "hombres": 254571,  "mujeres": 259142  },
    {"provincia": "Córdoba",                "hombres": 390559,  "mujeres": 405052  },
    {"provincia": "Coruña, A",              "hombres": 541292,  "mujeres": 585904  },
    {"provincia": "Cuenca",                 "hombres": 102583,  "mujeres": 101258  },
    {"provincia": "Gipuzkoa",               "hombres": 350799,  "mujeres": 366035  },
    {"provincia": "Girona",                 "hombres": 376936,  "mujeres": 376118  },
    {"provincia": "Granada",                "hombres": 451907,  "mujeres": 465390  },
    {"provincia": "Guadalajara",            "hombres": 128952,  "mujeres": 124734  },
    {"provincia": "Huelva",                 "hombres": 257699,  "mujeres": 262318  },
    {"provincia": "Huesca",                 "hombres": 112626,  "mujeres": 110283  },
    {"provincia": "Jaén",                   "hombres": 323861,  "mujeres": 330309  },
    {"provincia": "León",                   "hombres": 233664,  "mujeres": 245731  },
    {"provincia": "Lleida",                 "hombres": 220719,  "mujeres": 215310  },
    {"provincia": "Lugo",                   "hombres": 164605,  "mujeres": 174781  },
    {"provincia": "Madrid",                 "hombres": 3087022, "mujeres": 3349974 },
    {"provincia": "Málaga",                 "hombres": 800767,  "mujeres": 828206  },
    {"provincia": "Murcia",                 "hombres": 735434,  "mujeres": 731854  },
    {"provincia": "Navarra",                "hombres": 317885,  "mujeres": 322591  },
    {"provincia": "Ourense",                "hombres": 153043,  "mujeres": 165348  },
    {"provincia": "Palencia",               "hombres": 82232,   "mujeres": 83803   },
    {"provincia": "Palmas, Las",            "hombres": 548849,  "mujeres": 549557  },
    {"provincia": "Pontevedra",             "hombres": 458114,  "mujeres": 489260  },
    {"provincia": "Rioja, La",              "hombres": 156733,  "mujeres": 160320  },
    {"provincia": "Salamanca",              "hombres": 165379,  "mujeres": 174016  },
    {"provincia": "Santa Cruz de Tenerife", "hombres": 494354,  "mujeres": 507546  },
    {"provincia": "Segovia",                "hombres": 79355,   "mujeres": 78215   },
    {"provincia": "Sevilla",                "hombres": 950587,  "mujeres": 990893  },
    {"provincia": "Soria",                  "hombres": 46077,   "mujeres": 44929   },
    {"provincia": "Tarragona",              "hombres": 397730,  "mujeres": 397371  },
    {"provincia": "Teruel",                 "hombres": 70605,   "mujeres": 68327   },
    {"provincia": "Toledo",                 "hombres": 349553,  "mujeres": 343818  },
    {"provincia": "Valencia/València",      "hombres": 1250165, "mujeres": 1293150 },
    {"provincia": "Valladolid",             "hombres": 256999,  "mujeres": 269289  },
    {"provincia": "Zamora",                 "hombres": 90888,   "mujeres": 92548   },
    {"provincia": "Zaragoza",               "hombres": 469456,  "mujeres": 486550  }];

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var result = yield db.collection('poblacion').insertMany(poblacion);
    assert(result.insertedCount, 50);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Consultar

Para consultar los documentos que acabamos de insertar podemos utilizar cualquiera de los métodos del driver de MongoDB: find(), findOne() o aggregate(). En este ejemplo, utilizamos findOne() para obtener los datos de una sola provincia:

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

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var poblacion = yield db.collection('poblacion').findOne({"provincia": "Madrid"});
    assert(poblacion.mujeres, 3349974);
    assert(poblacion.hombres, 3087022);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Si utilizamos find o aggregate que devuelven varios registros tenemos dos opciones, utilizar toArray() o recoger el cursor y procesarlo. Veamos cómo sería en cada caso:

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

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var poblacion = yield db.collection('poblacion').find({}).toArray();
    assert(poblacion.length, 50);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Si queremos utilizar el modelo basado en el cursor, entonces tenemos que llamar a hasNext() y next() con su correspondiente yield.

"use strict";
var MongoClient = require('mongodb').MongoClient;
var assert = require('assert');
var co = require('co');

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var cursor = db.collection('poblacion').find({});
    var n = 0;
    while (yield cursor.hasNext()) {
        let provincia = yield cursor.next();
        n++;
    }
    assert(n, 50);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Modificar documentos

Si queremos modificar la colección utilizaremos cualquiera de los métodos dispuestos para este fin: update(), updateMany(), updateOne(), findOneAndReplace(), o findOneAndUpdate(). Como en el caso de la inserción, lo que obtendremos como retorno será el objeto que hubiéramos recibido como parámetro del callback

Vamos a utilizar este ejemplo para mostrar una limitación de este modelo, ya que no podemos incluir yield dentro de una función que no sea generadora, por ejemplo, la que utilizaríamos como parámetro de un sencillo forEach. Esa función pasada al método de la matriz no es una función generadora, por lo que no podemos utilizar co y yield sin complicar demasiado el código. La solución es trivial, utilizar un sencillo bucle en vez de estos métodos. En nuestro caso hemos optado por for of para aprovechar todas las ventajas de Ecmascript 6.

En este ejemplo también vamos a mostrar como realizar operaciones en paralelo y esperar a que termine la ejecución de todas las instrucciones. Para ello crearemos una matriz de promesas y llamaremos a yield con esa matriz. De esta forma se interrumpe la ejecución hasta que todas las promesas han sido ejecutadas.


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

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var collection = db.collection('poblacion')
    var poblacionTotal = yield collection.aggregate([
        {
            $project: {
                "provincia": 1,
                "total": { $add: [ "$hombres", "$mujeres"] }
            }
        }
    ]).toArray();

    var result = [];
    // No funciona, intentamos ejecutar yield 
    // en una función que no es generadora
    // poblacionTotal.forEach(function(doc) {
    //     result.push( collection.update({"provincia": doc.provincia}, 
    //                                    {$set: {"total": doc.total}}));
    //     yield result;
    // });
    for (var doc of poblacionTotal) {
        result.push(collection.update({"provincia": doc.provincia}, 
                                      {$set: {"total": doc.total}}));
    }
    yield result;

    var provincias = yield collection.find({}).sort({total: 1}).toArray();
    assert(provincias[0].provincia, 'Soria');
    assert(provincias[0].total, 91006);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Ejecución de comandos

Podríamos seguir con el resto de operaciones, como el borrado de documentos, si bien no difieren en lo sustancial a lo ya visto. También podríamos extendernos en algunas de los métodos para el manejo de cursores, pero en cualquier caso, conociendo la limitación que tenemos en el uso de yield en funciones que pasemos como parámetro y que por lo tanto, no son generadoras, nos podemos imaginar que los métodos each() o forEach() de los cursores no se pueden utilizar con este modelo.

Para cerrar vamos a incluir un sencillo ejemplo del uso de comandos. Cuando se descubre la capacidad y versatilidad del uso de comandos en MongoDB, se abre ante nosotros un enorme campo de actuación más allá de las más comunes órdenes para el manejo de documentos. Todo es posible con comandos, aunque en general preferiremos utilizar instrucciones más sencillas, pero no siempre será posible. Este ejemplo nos permite indicar que cualquier método del driver de MongoDB que devuelva una promesa es susceptible de utilizarse con este modelo:

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

co(function* () {

    var url = 'mongodb://localhost:27017/test';
    var db = yield MongoClient.connect(url);
    console.log("Connected correctly to server");

    var stat = yield db.admin().command({serverStatus:1});

    console.log(stat);

    db.close();

}).catch(function (err) {
    console.error(err);
});

Como se ha podido observar, el uso de las funciones generadoras y co() ofrecen una forma bastante sencilla de gestionar la asincronía de las llamadas al driver de MongoDB. Es verdad que en nuestro código aparecerán bastantes yield y que seguramente no comprendamos a primera vista su funcionamiento si no conocemos este patrón de funcionamiento, pero una vez nos habituemos a él podremos escribir código más comprensible que con los interminables callbacks.

Novedades

JSDayES – Vídeos

Si te lo has perdido o si quieres volver a ver las charlas y talleres del impresionante JSDayES 2017, aquí tienes todos los vídeos. Ponentes de primer orden, tanto nacionales como internacionales, muestran todos los aspectos de Javascript.

The CITGM Diaries by Myles Borins [video]

Myles Borins de Google, miembro del CTC de Node.js (Node.js Core Technical Committee), nos cuenta (en inglés, por supuesto) como funciona CITGM (Canary In The Gold Mine), una herramienta que permite obtener un módulo de NPM y probarlo usando una versión específica de NodeJS.

Debate: Tecnologías de Front Web [vídeo]

Desde las principales comunidades de desarrollo de tecnologías de front (Madrid JS, Polymer Madrid, Angular Madrid y VueJS Madrid) se ha organizado este debate que pretende ser un ejercicio de sentido común en relación a las tecnologías de front actuales centradas en componentes.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Algunos operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.

Cómo diferenciar arrow function de function

En un reciente artículo Javier Vélez Reyes hace patente las principales diferencias entre las funciones tradicionales y las funciones flecha, ya que ambos modelos no son equivalentes e intercambiables. Veamos cómo es posible saber si una función ha sido construida por medio de la instrucción function o como una arrow function.