Seleccionar página

NeDBHace unos días nos encontrábamos analizando la mejor solución para guardar una pequeña cantidad de datos que teníamos manejar para un crawler. Básicamente necesitábamos almacenar las solicitudes de trabajos y las páginas visitadas con alguna información adicional. Sinceramente, nos daba alguna pereza instalar un sistema de bases de datos para este proyecto, ya que queríamos que fuera extremadamente sencillo y ligero. Tras estudiar varias opciones, descubrimos NeDB, una base de datos escrita completamente en Javascript, sin ningún tipo de dependencia binaria, y que expone un interfaz de programación muy similar a MongoDB. Nos sorprendió ver la velocidad a la que se mueve (no es un MongoDB, pero es bastante rápida). Nos impresionó cuando vimos que funciona tanto en NodeJS y en Chrome, Firefox, Safari e IE9+.

Si ya se tiene conocimientos de MongoDB su uso es realmente sencillo, por el contrario, si se carece de experiencia previa con este motor de base de datos o con bases de datos NoSQL es posible que resulte un poco extraño al principio. Vamos a ver paso a paso las principales características que nos ofrece este sencillo, pero útil paquete:

Instalación

La forma más sencilla de instalarlo es por medio de NPM con la siguiente instrucción:

npm install nedb --save

No es un paquete muy grande y sus dependencias son bastante «razonables».

Hay unas cuantas decenas de paquetes que extienden, complementan o interaccionan con NeDB. Os recomendamos realizar algunas búsquedas en NPM. Da muestra de la extensión de su uso y da bastante confianza sobre su estabilidad.

Crear un datastore

Los datastore son el equivalente a las colecciones en MongoDB. Si sólo necesitamos un datastore en memoria, su creación es extremadamente sencilla:

var Datastore = require('nedb'),
    db = new Datastore();

Esta es una solución sencilla, pero muy probablemente querremos que se gestione la persistencia de estos datos en un fichero y de esta forma poder recuperarlos posteriormente. Para ello sólo tenemos que pasar algunas opciones a la hora de crear el datastore:

var Datastore = require('nedb'),
    db = new Datastore({filename: __dirname + '/data/example.dat', autoload: true});

Se pueden crear tantos datastore como necesitemos. A partir de aquí los utilizaremos de una forma muy similar a las colecciones de MongoDB.

Insertar documentos

Para insertar documentos de forma individual podemos llamar a insert de esta forma:

db.insert({n: (0 | (Math.random() * 10000)), now: new Date()}, function(err, record) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(record);
    // El resultado debe ser similar a este
    // { n: 9522,
    //   now: Sun Nov 01 2015 21:42:29 GMT+0100 (Hora estándar romance),
    // _id: 'Opc90LgGqYSjMtQ4' }
});

Algunas advertencias:

  • Hay un campo especial denominado _id que de no pasarlo se genera automáticamente y no puede repetirse en un datastore.
  • No es posible crear un documento con una clave que contenga los caracteres ‘$’ o ‘.’. Hay algunos ejemplos en Internet sobre cómo utilizar caracteres alternativos a estos que están reservados

Podemos utilizar el mismo método para insertar varios documentos, simplemente tenemos que pasarlos dentro de una matriz:

var docs = [
    {n: 1, name: 'one'},
    {n: 2, name: 'two'},
    {n: 3, name: 'three'},
    {n: 4, name: 'four'},
    {n: 5, name: 'five'}
];
db.insert(docs, function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Consultar documentos

Para obtener los documentos almacenados en el datastore utilizaremos el método find pasando como parámetro algún criterio y obtendremos todos aquellos que lo cumplan:

db.find({n: 3}, function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Si queremos obtener todos los documentos sólo tenemos que pasar un objeto vacío y, por lo tanto, todos los documentos cumplen esta condición:

db.find({}, function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Hay bastantes opciones para realizar búsquedas en subdocumentos, dentro de matrices en los documentos, por expresiones regulares, etc. Consulta la documentación de NeDB o, incluso la de MongoDB, para tener una referencia completa de lo que se puede llegar a hacer. Aquí, a modo de resumen, te indicamos sólo algunas de ellas:

  • $or y $and, realizar comparaciones lógicas que utilizan de esta forma { $or: [query1, query2, ...] }.
  • $not, invierte el resultado de una consulta { $not: query }
  • $where, permite ejecutar una función de esta forma { $where: function () {} } el objeto es this, el retorno debe ser true o false
  • $lt, $lte: menor que, menor o igual que {n: {$lt: 3}}
  • $gt, $gte: mayor que, mayor o igual que {n {$gt: 3}}
  • $in: el valor debe estar dentro de la matriz
  • $ne, $nin: no igual, no es un miembro de
  • $exists: comprueba la existencia de una propiedad en el objeto
  • $regex: comprueba si la cadena coincide con la expresión regular (no acepta opciones, como MongoDB)
  • $size: comprueba el tamaño de una matriz
db.find({n: {$lt:3}}, function(err, records) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(records);
});

Configurar los resultados

Obtener un sólo resultado

Si queremos asegurarnos que sólo vamos a obtener un resultado podemos utilizar findOne de esta forma:

db.findOne({n: 3}, function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Ordenar

Si queremos obtener los valores con un determinado orden podemos utilizar los métodos sort y exec de la siguiente forma:

db.find({n: {$lt:3}}).sort({name:-1}).exec(function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Paginar

También podemos paginar los resultados, saltándonos algunos valores (skip) y obteniendo un determinado número de resultados (limit):

db.find({}).sort({name:1}).skip(1).limit(2).exec(function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Proyección

Otra característica que tenemos a nuestra disposición es definir que parte de los documentos queremos obtener, limitando de esta forma el tamaño de los documentos que vamos utilizar:

db.find({}, {name: 1}, function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Contar

Cuando sólo queremos saber cuántos documentos se obtendrían con un determinado filtro utilizaremos:

db.count({n: {$lt:3}},function(err, record) {
    if (err) {
        console.error(err);
        process.exit(0);
    }
    console.log(record);
});

Actualizar los documentos

Para actualizar un documento tenemos que utilizar el método update de la siguiente forma:

db.update({n: 3}, {$set: {nombre: 'tres'}}, {}, function(err, num) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(num);
    db.find({n: 3}, function(err, result) {
        if (err) {
            console.error(err);
            return;
        }
        console.log(result);
    });
});

Como podemos observar se ha utilizado la cláusula $set para indicar que se quiere actualizar el documento con este nuevo valor. También se pueden utilizar:

  • $unset para eliminar elementos
  • $inc para incrementar el valor de una propiedad del documento
  • $push, $pop, $addToSet, $pull y $each para trabajar con las matrices del documento.

En este sencillo ejemplo no hemos pasado ninguna opción y por eso podemos ver como tercer parámetro un objeto vacio {}, pero podemos utilizar estas opciones para determinar con mayor precesión el comportamiento de update:

  • {multi: true} (por defecto es false) permite podificar múltiples documentos
  • {upsert: true} (por defecto es false) añade un nuevo documento si no se encuentra el documento. En este caso la función callback recibe un tercer parámetro con los nuevos documentos

Borrar documentos

Para eliminar documentos del datastore utilizaremos el método remove:

db.remove({n: {$gte:  4}}, {multi: true}, function(err, num) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(num);
    db.find({}, function(err, result) {
        if (err) {
            console.error(err);
            return;
        }
        console.log(result);
    });
});

En este caso la única opción que podemos pasar como tercer parámetro es {multi: true} que indica que se borren todos los documentos que coincidan con el filtro que hemos pasado. Si no se pasa este parámetro sólo se borrar el primer documento que coincida con el filtro.

Optimizaciones y algunas caracaterísticas internas

Antes de concluir tres aspectos que debemos tener en cuenta:

Índices

Es posible definir índices, lo cual hará que los resultados se obtengan más rápidamente. Para ello podemos utilizar ensureIndex con tres opciones:

  • fieldName (requerido): nombre de la propiedad sobre la que vamos a crear el índice
  • unique (opcional, por defecto false): fuerza que los valores del índice sean únicos y por lo tanto dará un error al intentar crear un valor duplicado
  • sparse (optional, por defecto false): no indexa los documentos que no tenga la propiedad que indexamos
  • db.ensureIndex({fieldName: 'name', unique: true}, function(err, num) {
        if (err) {
            console.error(err);
            return;
        }
        console.log(num);
    });
    

    Los índices sólo es necesario crearlos una vez si hemos definido el datastore con persistencia. Si por algún motivo queremos eliminar un índice creado, utilizaremos removeIndex pasando como parámetro el nombre del campo sobre el que hemos creado el índice.

    Formato del fichero de datos

    El formato interno que utilizar NeDB es bastante curioso, sólo se añaden valores y cualquier borrado o modificación se anexa al fichero que contiene los valores en texto plano con formato JSON separados en líneas. Es posible encriptar el contenido por medio de los eventos afterSerialization y beforeDeserialization, aunque su uso sobrepasa esta introducción.

    Compactar el fichero de datos

    Al ser un fichero en el que no se modifican los valores, sólo se van añadiendo, es posible que crezca rápidamente. Para controlar su tamaño podemos compactarlo por medio de db.persistence.compactDatafile().

    Si queremos que esta compactación se ejecute cada un determinado periodo de tiempo podemos utilizar db.persistence.setAutocompactionInterval(interval). Este intervalo se puede detener con db.persistence.stopAutocompaction().

    Conclusión

    Claramente NeDB no es un motor de base de datos, es una base de datos que podemos incrustar (empotrar) en nuestro código para labores auxiliares y por ello resulta muy interesante. Además, su compatibilidad con MongoDB nos hace más sencillo migrar posteriormente desde NeDB a ese motor si los requisitos de rendimiento, seguridad o volumen así nos lo exigen.

    Como decíamos al principio, existen numerosas extensiones y paquetes que complementan a NeDB, por lo que podemos utilizarlo desde para almacenar sesiones en Express o Connect, para integrarlo con Sails.js, y así un largo etcétera.

    NeDB es una buena herramienta para tener a mano y utilizarla en cuanto necesitemos almacenar información en nuestra aplicación y no queremos o no podemos utilizar un sistema de base de datos como tal.

Novedades

Limitar el tamaño de un Map

Limitar el tamaño de un objeto Map no parece una idea muy razonable, pero cuando tu programa se ejecuta sin interrupción durante días, semanas, meses e inclusos años, es muy importante controlar el tamaño de la memoria utilizada para evitar problemas inesperados. Una simple función memoize puede llegar a almacenar mucha más información de la que puedes pensar. Aquí te contamos como limitar el tamaño de un objeto Map para estas situaciones.

Cómo conseguir un objeto Map ordenado

Mantener un objeto Map con su contenido ordenado no es algo tan sencillo como parece. Por defecto, Map guarda los datos en el mismo orden en el que han sido creados en el objeto. Para conseguir que el contenido se muestre ordenado tendremos que explorar varias interesantes alternativas que nos descubrirán algunas de características interesantes de estas estructuras de datos.

¿Es una función nativa de Javascript?

Comprobar si una determinada función es una función nativa de Javascript o es una función escrita en código es algo más complicado de lo que pueda parecer a primera vista. No hay grandes diferencias entre una función nativa y una escrita por nosotros, por lo que tenemos que buscar mecanismos algo indirectos para poder diferenciarlas.