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 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 características.

    Bookshelf.js se encuentra entre los paquetes ORM de Node.js más populares. Proviene del Knex.js, que es un generador de consultas flexible que funciona con PostgreSQL, MySQL y SQLite3. Bookshelf.js se basa en esto proporcionando 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 admite múltiples back-end de bases de datos, como MySQL, PostgreSQLy 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 node, incluida la conexión a una base de datos, la creación de modelos y el guardado / carga de 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 Knex manualmente junto con Bookshelf:

    $ 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:

    $ 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, durante el uso --save-dev para una base de datos más pequeña como SQLite para su uso durante el desarrollo.

    $ 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.

    Conectarse a una 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 bookshelf instancia, necesitará crear una knex instancia primero, así:

    var knex = require('knex')({
        client: 'sqlite3',
        connection: {
            filename: './db.sqlite'
        }
    });
    
    var bookshelf = require('bookshelf')(knex);
    

    Y ahora puedes usar el bookshelf instancia para crear sus modelos.

    Configurar las tablas

    Knex, como dice su propio sitio web, es un generador de consultas SQL con “baterías incluidas”, por lo que puede hacer casi cualquier cosa a través de Knex que desee hacer con declaraciones SQL sin procesar. Una de estas características importantes es la creación y manipulación de tablas. Knex se puede utilizar 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.).

    Primero 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 de esquemas funciones, como table.increments(), table.string()y table.date(). Para cada modelo que cree, deberá hacer algo como esto para cada uno:

    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’, ‘rol’ 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 ‘correo electrónico’) o un valor predeterminado (‘admin’ para la columna ‘rol’).

    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 también configurar el hasTimestamps propiedad a true en su modelo (consulte ‘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 el Documentación de Knex para más detalles.

    Creando un modelo

    Una de mis quejas sobre Bookshelf es que siempre necesitas un bookshelf instancia para crear un modelo, por lo que estructurar algunas aplicaciones puede ser un poco complicado si mantiene todos sus modelos en archivos diferentes. Personalmente, prefiero simplemente hacer bookshelf un uso global 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:

    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. Primero que nada, 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 hizo en otro lugar.

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

    • tableName: Una cadena que le dice al modelo dónde guardar y cargar datos en la base de datos (requerido)
    • hasTimestamps: Un valor booleano que le dice al modelo si necesitamos created_at y updated_at marcas de tiempo
    • 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:

    User.byEmail('[email protected]').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 .get() método.

    Soporte ES6

    En el momento de escribir este artículo, Bookshelf no parece tener soporte completo para ES6 (consulte este problema). Sin embargo, aún puede escribir gran parte del código de su modelo utilizando las nuevas clases de ES6. Usando el modelo de arriba, podemos volver a crearlo usando el nuevo class sintaxis como esta:

    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 utilizar exactamente como el anterior. Este método no le dará ninguna ventaja funcional, pero es más familiar para algunas personas, así que aprovéchelo si lo desea.

    Colecciones

    En Bookshelf también necesita crear un objeto separado para las colecciones de un modelo determinado. Entonces, si desea realizar una operación en múltiples Users al mismo tiempo, por ejemplo, necesita crear un Collection.

    Continuando con nuestro ejemplo anterior, así es como crearíamos el Users Collection objeto:

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

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

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

    Aún mejor, ahora podemos usar algunos métodos de modelo agradables 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():

    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.

    Ampliando sus modelos

    Como desarrollador, uno de los principios más importantes que he seguido es el SECO (No se repita) principio. 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 Bookshelf’s .extend() método, puede heredar todas las propiedades, métodos de instancia y métodos de clase de un modelo base. De esta manera, puede crear y aprovechar métodos base que aún no se proporcionan, como .find(), .findOne()etc.

    Un gran ejemplo de extensión del modelo es el estantería-modelo base project, 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í:

    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 métodos útiles.

    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 más obvia forma es simplemente llamar .save() en una instancia de modelo.

    var user = new User();
    user.set('name', 'Joe');
    user.set('email', '[email protected]');
    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 desde una llamada de consulta.

    La otra opción es utilizar el .forge() e inicializarlo con datos. ‘Forge’ es solo una forma abreviada de crear un nuevo modelo (como new User()). Pero de esta manera no necesita una línea adicional para crear el modelo antes de iniciar la consulta / guardar la cadena.

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

    var data = {
        name: 'Joe',
        email: '[email protected]',
        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 data es en realidad JSON entrante o algo así.

    Carga de modelos

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

    Mientras .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 lugar.

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

    User.forge({email: '[email protected]'}).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 name y age columnas también).

    Al igual que en SQL antiguo simple, 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:

    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 el withRelations opción para cargar automáticamente modelos relacionados, que veremos en la siguiente sección.

    Relaciones de modelo

    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 usando el belongsTo(), hasMany()y hasOne() (entre otros) métodos.

    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í:

    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 plugin de registro aquí, lo que me permite hacer referencia al modelo de dirección con una cadena.

    los hasMany() y belongsTo() Los métodos 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 solo 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 al user_id columna en la tabla de direcciones.

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

    User.forge({email: '[email protected]'}).fetch({withRelated: ['addresses']})
    .then(function(user) {
        console.log('Got user:', user.get('name'));
        console.log('Got addresses:', user.related('addresses'));
    });
    

    Si tuviéramos que buscar el modelo de usuario sin el withRelated opción 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 propias JOIN SQL 🙂

    El bueno

    Bookshelf es una de esas bibliotecas que parece intentar no hincharse demasiado y simplemente se apega a las funciones principales. Esto es genial porque las características que existen funcionan muy bien.

    Bookshelf también tiene una API potente y agradable que te permite crear fácilmente tu aplicación sobre ella. Por lo tanto, no tiene que luchar con métodos de alto nivel que hacen suposiciones incorrectas sobre cómo se usarían.

    El malo

    Si bien creo que es bueno que Bookshelf / Knex le proporcione algunas funciones de nivel inferior, sigo pensando que hay margen de mejora. Por ejemplo, toda la configuración de la tabla / esquema se deja en sus manos, 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, lo que no es tan fácil de leer y depurar.

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

    Conclusión

    En general, me he vuelto un 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 obstáculo para muchos desarrolladores que están acostumbrados a usar ORM que hacen casi todo por sacarlos 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 cantidad posible de la API principal en este artículo, todavía hay bastantes características que no pude tocar, así que asegúrese de revisar el documentación del proyecto para más información.

    ¿Ha utilizado Bookshelf.js o Knex.js? ¿Qué piensas? ¡Háznoslo saber en los comentarios!

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *