NeDB: una base de datos ligera de JavaScript

Cuando piensa en una base de datos, lo primero que se le viene a la cabeza puede ser MySQL, MongoDB o PostgreSQL. Si bien todas estas son excelentes opciones para almacenar...

Cuando piensa en una base de datos, lo primero que se le viene a la cabeza puede ser MySQL, MongoDB o PostgreSQL. Si bien todas estas son excelentes opciones para almacenar datos, todas están sobrecargadas para la mayoría de las aplicaciones.

Considere una aplicación de chat de escritorio escrita con el marco Electrón en JavaScript. Si bien los datos del chat (mensajes, contactos, historial, etc.) probablemente se originarían en un servidor API, también deberían almacenarse localmente dentro de la aplicación. Potencialmente, podría tener miles de mensajes, todos los cuales deberían almacenarse para facilitar el acceso y la búsqueda.

Entonces, ¿Qué haces? Una opción es almacenar todos estos datos en un archivo en algún lugar y simplemente buscarlos cada vez que necesite recuperarlos, pero esto puede ser ineficiente. Otra opción es simplemente no almacenar en caché los datos localmente y hacer una llamada al servidor API cada vez que necesite más datos, pero entonces su aplicación responderá menos y utilizará muchos más datos de red.

Una mejor idea es utilizar una base de datos integrada/ligera, como NeDB. Esto tiene más sentido porque su aplicación no atenderá a miles de usuarios ni manejará gigabytes de datos.

NeDB es muy parecido a SQLite en el sentido de que es una versión más pequeña e integrable de un sistema de base de datos mucho más grande. En lugar de ser un almacén de datos SQL más pequeño, NeDB es un almacén de datos NoSQL más pequeño que imita [MongoDB] (https://www.mongodb.org/).

Una base de datos liviana generalmente almacena sus datos en la memoria o en un archivo de texto sin formato (con índices para búsquedas rápidas). Esto ayuda a reducir el espacio total de la base de datos en el sistema, lo cual es perfecto para aplicaciones más pequeñas. A modo de comparación, el archivo tar de MySQL (para Mac OSX) tiene 337 MB, mientras que NeDB (sin comprimir, sin minimizar) tiene solo alrededor de 1,5 MB.

Una de las mejores cosas de NeDB específicamente es que su API es un subconjunto de la API de MongoDB, por lo que si está familiarizado con MongoDB, no debería tener problemas para trabajar con NeDB después de la configuración inicial.

Nota: a partir de la versión 1.8.0, NeDB aún no se ha actualizado a algunos de los nuevos nombres de métodos de Mongo, como insertOne, insertMany y la eliminación de findOne.

Primeros pasos con NeDB

Primero, instale el módulo con NPM:

1
$ npm install nedb --save

El módulo está escrito en JavaScript puro, por lo que no debería haber problemas para compilar complementos nativos, como ocurre a veces con los controladores MongoDB.

Si planea usarlo en el navegador, use Bower para instalar:

1
$ bower install nedb

Como todos los clientes de base de datos, el primer paso es conectarse a la base de datos de back-end. Sin embargo, en este caso no hay una aplicación externa a la que conectarse, por lo que solo debemos indicarle la ubicación de sus datos. Con NeDB, tiene algunas opciones para guardar sus datos. La primera opción es guardar los datos en memoria:

1
2
3
4
var Datastore = require('nedb');
var db = new Datastore();

// Start issuing commands right away...

Esto comenzará sin datos y, cuando salga de la aplicación, se perderán todos los datos guardados. Aunque es genial para usar durante las pruebas o sesiones más cortas (como en el navegador).

O la otra opción es guardar los datos en un archivo. La diferencia aquí es que debe especificar la ubicación del archivo y cargar los datos.

1
2
3
4
5
6
var Datastore = require('nedb');
var db = new Datastore({ filename: 'path/to/your/file' });

db.loadDatabase(function(err) {
    // Start issuing commands after callback...
});

Si no desea llamar a db.loadDatabase para cada base de datos que cargue, siempre puede usar la opción autoload: true también.

Una cosa importante a tener en cuenta es que cada archivo es el equivalente a una recopilación en MongoDB. Entonces, si tiene varias colecciones, deberá cargar varios archivos al inicio. Así que tu código podría verse así:

1
2
3
4
var Datastore = require('nedb');
var users = new Datastore({ filename: 'users.db', autoload: true });
var tweets = new Datastore({ filename: 'tweets.db', autoload: true });
var messages = new Datastore({ filename: 'messages.db', autoload: true });

Guardando datos {#guardando datos}

Después de cargar sus datos desde archivos (o crear almacenamiento en memoria), querrá comenzar a guardar datos.

Al igual que los controladores de Mongo, usará insert para crear un nuevo documento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var Datastore = require('nedb');
var users = new Datastore();

var scott = {
    name: 'Scott',
    twitter: '@ScottWRobinson'
};

users.insert(scott, function(err, doc) {
    console.log('Inserted', doc.name, 'with ID', doc._id);
});

// Prints to console...
// (Note that ID will likely be different each time)
//
// "Inserted Scott with ID wt3Nb47axiOpme9u"

Esta inserción se puede ampliar fácilmente para guardar varios documentos a la vez. Usando el mismo método, simplemente pase una serie de objetos y cada uno se guardará y se le devolverá en la devolución de llamada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var Datastore = require('nedb');
var users = new Datastore();

var people = [];

var scott = {
    name: 'Scott Robinson',
    age: 28,
    twitter: '@ScottWRobinson'
};

var elon = {
    name: 'Elon Musk',
    age: 44,
    twitter: '@elonmusk'
};

var jack = {
    name: 'Jack Dorsey',
    age: 39,
    twitter: '@jack'
};

people.push(scott, elon, jack);

users.insert(people, function(err, docs) {
    docs.forEach(function(d) {
        console.log('Saved user:', d.name);
    });
});

// Prints to console...
//
// Saved user: Scott Robinson
// Saved user: Elon Musk
// Saved user: Jack Dorsey

La actualización de documentos existentes funciona de la misma manera, excepto que deberá proporcionar una consulta para indicarle al sistema qué documento (s) debe actualizarse.

Cargando datos

Ahora que tenemos un montón de datos guardados, es hora de recuperarlos de la base de datos. De nuevo, seguiremos la misma convención que Mongo con el método find:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.findOne({ twitter: '@ScottWRobinson' }, function(err, doc) {
    console.log('Found user:', doc.name);
});

// Prints to console...
//
// Found user: Scott Robinson

Y nuevamente, podemos usar una operación similar para recuperar múltiples documentos. Los datos devueltos son solo una matriz de documentos coincidentes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.find({ age: { $lt: 40 }}, function(err, docs) {
    docs.forEach(function(d) {
        console.log('Found user:', d.name);
    });
});

// Prints to console...
//
// Found user: Jack Dorsey
// Found user: Scott Robinson

Es posible que haya notado en este último ejemplo de código que NeDB, como era de esperar, es capaz de realizar consultas más complejas, como comparaciones de números. Los siguientes operadores están disponibles para buscar/cotejar documentos:

  • $lt, $lte: menor que, menor que o igual
  • $gt, $gte: mayor que, mayor que o igual
  • $in: valor contenido en el arreglo
  • $nin: valor no contenido en el arreglo
  • $ne: no igual
  • $exists: comprueba la existencia (o inexistencia) de una propiedad dada
  • $regex: coincide con la cadena de una propiedad con regex

También puede utilizar las operaciones estándar de clasificación, limitación y omisión. Si no se le da una devolución de llamada al método find, entonces se le devolverá un objeto Cursor en su lugar, que luego puede usar para ordenar, limitar y omitir. Aquí hay un ejemplo de clasificación alfabética por nombre:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.find({}).sort({name: 1}).exec(function(err, docs) {
    docs.forEach(function(d) {
        console.log('Found user:', d.name);
    });
});

// Prints to console...
//
// Found user: Elon Musk
// Found user: Jack Dorsey
// Found user: Scott Robinson

Las otras dos operaciones, skip y limit, funcionan de manera muy similar a esta.

Hay bastantes operadores más soportados por los métodos find y findOne, pero no entraremos en todos ellos aquí. Puede leer en detalle sobre el resto de estas operaciones en la sección encontrar documentos del LÉAME.

Eliminación de datos

No hay mucho que decir sobre la eliminación de datos aparte de que funciona de manera similar a los métodos buscar. Utilizará los mismos tipos de consultas para encontrar los documentos relevantes en la base de datos. Los que se encuentran se eliminan.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.remove({ name: { $regex: /^Scott/ } }, function(err, numDeleted) {
     console.log('Deleted', numDeleted, 'user(s)');
});

// Prints to console...
//
// Deleted 1 user(s)

De forma predeterminada, el método remove solo elimina un único documento. Para eliminar varios documentos con una sola llamada, debe establecer la opción multi en true.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.remove({}, { multi: true }, function(err, numDeleted) {
     console.log('Deleted', numDeleted, 'user(s)');
});

// Prints to console...
//
// Deleted 3 user(s)

Datos de indexación

Al igual que cualquier otra base de datos, puede establecer índices en sus datos para una recuperación más rápida o para aplicar ciertas restricciones, como valores únicos. Para crear el índice, utilice el método ensureIndex.

Los tres tipos de índices soportados actualmente son:

  • único: asegúrese de que el campo dado sea único en toda la colección
  • sparse: no indexar documentos en los que el campo dado no está definido
  • expireAfterSeconds: elimina el documento después de la cantidad de segundos dada (tiempo de vida, o TTL)

El índice TTL es especialmente útil, en mi opinión, ya que le evita tener que escribir código para escanear y eliminar con frecuencia los datos que han caducado.

Esto puede ser útil, por ejemplo, con solicitudes de restablecimiento de contraseña. Si tiene un objeto PasswordReset almacenado en su base de datos, no querrá que sea válido para siempre. Para ayudar a proteger al usuario, probablemente debería caducar y eliminarse después de unos días. Este índice TTL puede encargarse de eliminarlo por usted.

En el siguiente ejemplo, hemos colocado la restricción ‘única’ en los identificadores de Twitter de los documentos. Esto significa que si un usuario se guarda con el mismo identificador de Twitter que otro usuario, se generará un error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var Datastore = require('nedb');
var users = new Datastore();

users.ensureIndex({ fieldName: 'twitter', unique: true });

var people = [];

var jack = {
    name: 'Jack Dorsey',
    age: 39,
    twitter: '@jack'
};

var jackSmith = {
    name: 'Jack Smith',
    age: 68,
    twitter: '@jack'
};

people.push(jack, jackSmith);

users.insert(people, function(err, docs) {
    console.log('Uh oh...', err);
});

// Prints to console...
//
// Uh oh... Can't insert key @jack, it violates the unique constraint

Llevándolo más lejos {#llevándolo más lejos}

Si bien la API de NeDB es fácil de usar y todo, su código puede volverse bastante difícil de trabajar si no está bien pensado y organizado. Aquí es donde entran los mapeadores de documentos de objetos ([que es como un ORM](http://stackoverflow.com/questions/12261866/what-is-the-difference- between-an-orm-and-an-odm)) jugar.

Usando el Camuflaje ODM (que creé), simplemente puede tratar los almacenes de datos NeDB como clases de JavaScript. Esto le permite especificar un esquema, validar datos, ampliar esquemas y más. Camo también funciona con MongoDB, por lo que puede usar NeDB en entornos de prueba/desarrollo y luego usar Mongo para su sistema de producción sin tener que cambiar nada de su código.

Aquí hay un ejemplo rápido de cómo conectarse a la base de datos, declarar un objeto de clase y guardar algunos datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var connect = require('camo').connect;
var Document = require('camo').Document;

class User extends Document {
    constructor() {
        super();

        this.name = String;
        this.age = Number;
        this.twitter = Sring;
    }

    get firstName() {
        return this.name.split(' ')[0];
    }
}

var scott = User.create({
    name: 'Scott Robinson',
    age: 28,
    twitter: '@ScottWRobinson'
});

var elon = User.create({
    name: 'Elon Musk',
    age: 44,
    twitter: '@elonmusk'
});

connect('nedb://memory').then(function(db) {
    return Promise.all([scott.save(), elon.save()]);
}).then(function(users) {
    users.forEach(function(u) {
        console.log('Saved user:', u.firstName);
    });

    return elon.delete();
}).then(function() {
    console.log('Deleted Elon!')
});

// Prints to console...
//
// Saved user: Scott
// Saved user: Elon
// Deleted Elon!

Hay mucho más en este ODM que lo que he mostrado aquí. Para obtener más información, consulta Este artículo o el LÉAME del proyecto para obtener la documentación.

Conclusión

Dado que NeDB es bastante pequeño (¡y bastante rápido!), es muy fácil agregarlo a casi cualquier proyecto. Y con Camo en la combinación, solo necesita unas pocas líneas de código para declarar objetos basados ​​en clases que son mucho más fáciles de crear, eliminar y manipular.

Si alguna vez ha utilizado NeDB en uno de sus proyectos, nos encantaría saberlo. ¡Cuéntanos en los comentarios!