Bookshelf.js: un ORM de Node.js

Uno de los recursos más comunes con los que interactuará en un lenguaje como Node.js (principalmente un lenguaje centrado en la web) son las bases de datos. Y siendo SQL el más c...

Uno de los recursos más comunes con los que interactuará en un lenguaje como Node.js (principalmente un lenguaje centrado en la web) son las bases de datos. Y dado que SQL es el más común de todos los diferentes tipos, necesitará una buena biblioteca que lo ayude a interactuar con él y sus muchas funciones.

Bookshelf.js es uno de los paquetes ORM de Node.js más populares. Proviene de Knex.js, que es un generador de consultas flexible que funciona con PostgreSQL, MySQL y SQLite3. Estantería.js se basa en esto al proporcionar funcionalidad para crear modelos de datos, formar relaciones entre estos modelos y otras tareas comunes necesarias al consultar una base de datos.

Bookshelf también es compatible con múltiples back-ends de bases de datos, como mysql, postgresql y SQLite. De esta manera, puede cambiar fácilmente las bases de datos cuando sea necesario, o usar una base de datos más pequeña como SQLite durante el desarrollo y Postgre en producción.

A lo largo de este artículo, le mostraré cómo aprovechar al máximo este ORM de nodo, lo que incluye conectarse a una base de datos, crear modelos y guardar/cargar objetos.

Instalar Bookshelf

Bookshelf es un poco diferente a la mayoría de los paquetes de Node en que no instala todas sus dependencias automáticamente. En este caso, debe instalar manualmente Knex junto con Bookshelf:

1
2
$ npm install knex --save
$ npm install bookshelf --save

Además de eso, debe elegir con qué base de datos desea usar Bookshelf. Tus opciones son:

Estos se pueden instalar con:

1
2
3
4
$ npm install pg --save
$ npm install mysql --save
$ npm install mariasql --save
$ npm install sqlite3 --save

Una cosa que tiendo a hacer con mis proyectos es instalar una base de datos de producción (como Postgre) usando --save, mientras uso --save-dev para una base de datos más pequeña como SQLite para usar durante el desarrollo.

1
2
$ npm install pg --save
$ npm install sqlite3 --save-dev

De esta manera, podemos cambiar fácilmente entre las bases de datos en producción y desarrollo sin tener que preocuparnos por inundar mi entorno de producción con dependencias innecesarias.

Conexión a una base de datos {#conexión a la base de datos}

Todas las funciones de nivel inferior, como conectarse a la base de datos, son manejadas por la biblioteca Knex subyacente. Entonces, naturalmente, para inicializar su instancia bookshelf, primero deberá crear una instancia knex, como esta:

1
2
3
4
5
6
7
8
var knex = require('knex')({
    client: 'sqlite3',
    connection: {
        filename: './db.sqlite'
    }
});

var bookshelf = require('bookshelf')(knex);

Y ahora puede usar la instancia bookshelf para crear sus modelos.

Preparando las Mesas

Knex, como dice su propio sitio web, es un generador de consultas SQL "baterías incluidas", por lo que puede hacer casi cualquier cosa a través de Knex que quiera hacer con declaraciones SQL sin formato. Una de estas características importantes es la creación y manipulación de tablas. Knex se puede usar directamente para configurar su esquema dentro de la base de datos (piense en la inicialización de la base de datos, la migración del esquema, etc.).

Entonces, antes que nada, querrá crear su tabla usando knex.schema.createTable(), que creará y devolverá un objeto de tabla que contiene un montón de [construcción del esquema](http://knexjs. org/#Schema-Building), como table.increments(), table.string() y table.date(). Para cada modelo que cree, deberá hacer algo como esto para cada uno:

1
2
3
4
5
6
7
8
knex.schema.createTable('users', function(table) {
    table.increments();
    table.string('name');
    table.string('email', 128);
    table.string('role').defaultTo('admin');
    table.string('password');
    table.timestamps();
});

Aquí puede ver que creamos una tabla llamada 'usuarios', que luego inicializamos con las columnas 'nombre', 'correo electrónico', 'función' y 'contraseña'. Incluso podemos ir un paso más allá y especificar la longitud máxima de una columna de cadena (128 para la columna 'email') o un valor predeterminado ('admin' para la columna 'role').

También se proporcionan algunas funciones de conveniencia, como timestamps(). Esta función agregará dos columnas de marca de tiempo a la tabla, created_at y updated_at. Si usa esto, considere establecer también la propiedad hasTimestamps en true en su modelo (vea 'Creación de un modelo' a continuación).

Hay bastantes opciones más que puede especificar para cada tabla/columna, por lo que definitivamente recomendaría consultar la [Documentación Knex] completa (http://knexjs.org) para obtener más detalles.

Crear un modelo {#crear un modelo}

Una de mis quejas sobre Bookshelf es que siempre necesita una instancia bookshelf inicializada para crear un modelo, por lo que estructurar algunas aplicaciones puede ser un poco complicado si guarda todos sus modelos en archivos diferentes. Personalmente, prefiero hacer que bookshelf sea global usando global.bookshelf = bookshelf, pero esa no es necesariamente la mejor manera de hacerlo.

De todos modos, veamos qué se necesita para crear un modelo simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var User = bookshelf.Model.extend({
    tableName: 'users',
    hasTimestamps: true,

    verifyPassword: function(password) {
        return this.get('password') === password;
    }
}, {
    byEmail: function(email) {
        return this.forge().query({where:{ email: email }}).fetch();
    }
});

Aquí tenemos un modelo bastante simple para demostrar algunas de las características disponibles. En primer lugar, la única propiedad requerida es tableName, que le dice al modelo dónde guardar y cargar datos en la base de datos. Obviamente, es bastante mínimo configurar un modelo, ya que toda la declaración del esquema ya se realizó en otro lugar.

En cuanto al resto de las propiedades/funciones, aquí hay un resumen rápido de lo que incluye Usuario:

  • tableName: una cadena que le dice al modelo dónde guardar y cargar datos en la base de datos (obligatorio)
  • hasTimestamps: un valor booleano que le dice al modelo si necesitamos las marcas de tiempo created_at y updated_at
  • verifyPassword: una función de instancia
  • byEmail: una función de clase (estática)

Entonces, por ejemplo, usaremos byEmail como una forma más corta de consultar a un usuario por su dirección de correo electrónico:

1
2
3
User.byEmail('[correo electrónico protegido]').then(function(u) {
    console.log('Got user:', u.get('name'));
});

Observe cómo accede a los datos del modelo en Bookshelf. En lugar de usar una propiedad directa (como u.name), tenemos que usar el método .get().

Compatibilidad con ES6

En el momento de escribir este artículo, Bookshelf no parece tener compatibilidad total con ES6 (ver este problema). Sin embargo, aún puede escribir gran parte de su código de modelo utilizando las nuevas clases de ES6. Usando el modelo de arriba, podemos volver a crearlo usando la nueva sintaxis de clase como esta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class User extends bookshelf.Model {
    get tableName() {
        return 'users';
    }

    get hasTimestamps() {
        return true;
    }

    verifyPassword(password) {
        return this.get('password') === password;
    }

    static byEmail(email) {
        return this.forge().query({where:{ email: email }}).fetch();
    }
}

Y ahora este modelo se puede usar exactamente como el anterior. Este método no te dará ninguna ventaja funcional, pero es más familiar para algunas personas, así que aprovéchalo si quieres.

Colecciones

En Bookshelf también necesita crear un objeto separado para las colecciones de un modelo dado. Entonces, si desea realizar una operación en varios ‘Usuarios’ al mismo tiempo, por ejemplo, debe crear una ‘Colección’.

Continuando con nuestro ejemplo anterior, así es como crearíamos el objeto Usuarios Colección:

1
2
3
var Users = bookshelf.Collection.extend({
    model: User
});

Bastante simple, ¿verdad? Ahora podemos consultar fácilmente a todos los usuarios con (aunque esto ya era posible con un modelo usando .fetchAll()):

1
2
3
Users.forge().fetch().then(function(users) {
    console.log('Got a bunch of users!');
});

Aún mejor, ahora podemos usar algunos buenos métodos de modelo en la colección como un todo, en lugar de tener que iterar sobre cada modelo individualmente. Uno de estos métodos que parece tener mucho uso, especialmente en aplicaciones web, es .toJSON():

1
2
3
4
5
exports.get = function(req, res) {
    Users.forge().fetch().then(function(users) {
        res.json(users.toJSON());
    });
};

Esto devuelve un objeto JavaScript simple de toda la colección.

Ampliación de sus modelos

Como desarrollador, uno de los principios más importantes que he seguido es el principio SECO (Don’t Repeat Yourself). Esta es solo una de las muchas razones por las que la extensión del modelo/esquema es tan importante para el diseño de su software.

Usando el método .extend() de Bookshelf, puede heredar todas las propiedades, métodos de instancia y métodos de clase de un modelo base. De esta forma, puede crear y aprovechar los métodos básicos que aún no se proporcionan, como .find(), .findOne(), etc.

Un gran ejemplo de extensión de modelo está en el proyecto estantería-modelbase, que proporciona muchos de los métodos faltantes que esperaría que fueran estándar en la mayoría de los ORM.

Si tuviera que crear su propio modelo base simple, podría verse así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var model = bookshelf.Model.extend({
    hasTimestamps: ['created_at', 'updated_at'],
}, {
    findAll: function(filter, options) {
        return this.forge().where(filter).fetchAll(options);
    },

    findOne: function(query, options) {
        return this.forge(query).fetch(options);
    },

    create: function(data, options) {
        return this.forge(data).save(null, options);
    },
});

Ahora todos sus modelos pueden aprovechar estos útiles métodos.

Guardar y actualizar modelos {#guardar y actualizar modelos}

Hay un par de formas diferentes de guardar modelos en Bookshelf, según sus preferencias y el formato de sus datos.

La primera, y la más obvia, es simplemente llamar a .save() en una instancia del modelo.

1
2
3
4
5
6
7
8
var user = new User();
user.set('name', 'Joe');
user.set('email', '[correo electrónico protegido]');
user.set('age', 28);

user.save().then(function(u) {
    console.log('User saved:', u.get('name'));
});

Esto funciona para un modelo que crea usted mismo (como el anterior), o con instancias de modelo que se le devuelven a partir de una llamada de consulta.

La otra opción es usar el método .forge() e inicializarlo con datos. 'Forge' es realmente una forma abreviada de crear un nuevo modelo (como nuevo usuario()). Pero de esta manera, no necesita una línea adicional para crear el modelo antes de iniciar la consulta/guardar cadena.

Usando .forge(), el código anterior se vería así:

1
2
3
4
5
6
7
8
9
var data = {
    name: 'Joe',
    email: '[correo electrónico protegido]',
    age: 28
}

User.forge(data).save().then(function(u) {
    console.log('User saved:', u.get('name'));
});

Esto realmente no le ahorrará ninguna línea de código, pero puede ser conveniente si los datos son en realidad JSON entrante o algo así.

Cargando modelos

Aquí hablaré sobre cómo cargar modelos desde la base de datos con Bookshelf.

Si bien .forge() realmente no nos ayudó mucho a guardar documentos, ciertamente ayuda a cargarlos. Sería un poco incómodo crear una instancia de modelo vacía solo para cargar datos de la base de datos, por lo que usamos .forge() en su lugar.

El ejemplo más simple de carga es simplemente buscar un solo modelo usando .fetch():

1
2
3
User.forge({email: '[correo electrónico protegido]'}).fetch().then(function(user) {
    console.log('Got user:', user.get('name'));
});

Todo lo que hacemos aquí es tomar un solo modelo que coincida con la consulta dada. Como puede imaginar, la consulta puede ser tan compleja como desee (como restringir también las columnas name y age).

Al igual que en SQL simple y antiguo, puede personalizar en gran medida la consulta y los datos que se devuelven. Por ejemplo, esta consulta solo nos dará los datos que necesitamos para autenticar a un usuario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var email = '...';
var plainTextPassword = '...';

User.forge({email: email}).fetch({columns: ['email', 'password_hash', 'salt']})
.then(function(user) {
    if (user.verifyPassword(plainTextPassword)) {
        console.log('User logged in!');
    } else {
        console.log('Authentication failed...');
    }
});

Llevando esto aún más lejos, podemos usar la opción withRelations para cargar automáticamente modelos relacionados, que veremos en la siguiente sección.

Relaciones de modelos {#relaciones de modelos}

En muchas aplicaciones, sus modelos deberán hacer referencia a otros modelos, lo que se logra en SQL utilizando claves externas. Bookshelf admite una versión simple de esto a través de relaciones.

Dentro de su modelo, puede decirle a Bookshelf exactamente cómo se relacionan otros modelos entre sí. Esto se logra utilizando los métodos belongsTo(), hasMany() y hasOne() (entre otros).

Entonces, digamos que tiene dos modelos, Usuario y Dirección. El Usuario puede tener varias Direcciones (una para envío, otra para facturación, etc.), pero una Dirección puede pertenecer a un solo Usuario. Dado esto, podríamos configurar nuestros modelos así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var User = bookshelf.Model.extend({
    tableName: 'users',
    
    addresses: function() {
        return this.hasMany('Address', 'user_id');
    },
});

var Address = bookshelf.Model.extend({
    tableName: 'addresses',
    
    user: function() {
        return this.belongsTo('User', 'user_id');
    },
});

Tenga en cuenta que estoy usando el complemento de registro aquí, que me permite referirme al modelo de dirección con una cadena .

Los métodos hasMany() y belongsTo() le dicen a Bookshelf cómo se relaciona cada modelo entre sí. El Usuario "tiene muchas" Direcciones, mientras que la Dirección "pertenece" a un único usuario. El segundo argumento es el nombre de la columna que indica la ubicación de la clave del modelo. En este caso, ambos modelos hacen referencia a la columna user_id en la tabla de direcciones.

Ahora podemos aprovechar esta relación usando la opción withRelated en los métodos .fetch(). Entonces, si quisiera cargar un usuario y todas sus direcciones con una sola llamada, podría hacer lo siguiente:

1
2
3
4
5
User.forge({email: '[correo electrónico protegido]'}).fetch({withRelated: ['addresses']})
.then(function(user) {
    console.log('Got user:', user.get('name'));
    console.log('Got addresses:', user.related('addresses'));
});

Si buscáramos el modelo de usuario sin la opción withRelated entonces user.related('addresses') simplemente devolvería un objeto Collection vacío.

Asegúrese de aprovechar estos métodos de relación, son mucho más fáciles de usar que crear sus propios SQL JOIN :)

Lo bueno

Bookshelf es una de esas bibliotecas que parece intentar no inflarse demasiado y simplemente se apega a las características principales. Esto es genial porque las características que están allí funcionan muy bien.

Bookshelf también tiene una API agradable y poderosa que le permite construir fácilmente su aplicación encima de ella. Por lo tanto, no tiene que lidiar con métodos de alto nivel que hicieron suposiciones deficientes sobre cómo se usarían.

Lo malo

Si bien creo que es bueno que Bookshelf/Knex le brinde algunas funciones de nivel inferior, sigo pensando que hay margen de mejora. Por ejemplo, toda la configuración de la tabla/esquema depende de usted, y no hay una manera fácil de especificar su esquema (como en un objeto JS simple) dentro del modelo. La configuración de la tabla/esquema debe especificarse en las llamadas a la API, que no es tan fácil de leer y depurar.

Otra queja mía es cómo omitieron muchos de los métodos auxiliares que deberían venir de serie con el modelo base, como .create(), .findOne(), .upsert() y validación de datos. Esta es exactamente la razón por la que mencioné anteriormente el proyecto bookshelf-modelbase, ya que llena muchos de estos vacíos.

Conclusión

En general, me he convertido en un gran fanático del uso de Bookshelf/Knex para el trabajo de SQL, aunque creo que algunos de los problemas que acabo de mencionar podrían ser un desvío para muchos desarrolladores que están acostumbrados a usar ORM que hacen casi todo. para ellos fuera de la caja. Por otro lado, para otros desarrolladores a los que les gusta tener mucho control, esta es la biblioteca perfecta para usar.

Si bien traté de cubrir la mayor parte posible de la API central en este artículo, todavía hay algunas características que no pude abordar, así que asegúrese de consultar la [documentación del proyecto] (http:/ /bookshelfjs.org/) para obtener más información.

¿Ha utilizado Bookshelf.js o Knex.js? ¿Qué piensas? ¡Cuéntanos en los comentarios! arios!*