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 *