En este blog hablaré un poco sobre los ORM’s, sus beneficios (pros) y limitaciones (cons), que es Sequelize y cómo usarlo.
Object-Relational Mapping
Es una técnica para convertir datos entre el sistema de tipos del lenguaje de programación y la base de datos. Como su nombre lo indica, esto va dirigido solamente a las base de datos relacional (SQL). Esto crea un efecto “objeto base de datos virtual” sobre la base de datos relacional, este efecto es lo que nos permite manipular la base de datos a través del código.
Object – Hace referencia al/los objeto(s) que podemos usar en nuestro lenguaje.
Relational – Hace referencia a nuestro Sistema Gestor de Base de Datos (MySQL, MSSQL, PostgreSQL).
Mapping – Hace referencia a la conexión entre el los objetos y las tablas.
tl;dr ORM es una técnica que nos permite hacer queries y manipular datos de la base de datos desde un lenguaje de programación.
Pros
- Abstracto: Diseño de una estructura o modelo aislado de la base de datos.
- Portable: Te permite transportar la estructura de tu ORM a cualquier DBMS.
- Anidación de datos: En caso de que una tabla tenga una o varias relaciones con otras.
Cons
- Lento: Si se compara el tiempo de respuesta entre un raw query y un query hecho por objetos, raw query es mucho mas rápido debido a que no existe una capa (mapping).
- Complejidad: Algunas veces necesitaremos hacer queries complejos, por suerte Sequelize te permite ejecutar raw queries.
Que es Sequelize ?
Sequelize es un ORM basado en promesas para Node.js. Soporta PostgreSQL, MySQL, SQLite y MSSQL, y entrega características sólidas de transacciones, relaciones entre tablas, mecanismos de migraciones y carga de datos, y más.
Porque decidí utilizar Sequelize ?
Sequelize maneja sus objetos como promesas, algo que va de la mano con el event loop de Node.js.
Ahora les mostraré cómo crear y migrar tablas, cargar datos, y cómo consultar estos datos. Si quieren checar el código, pueden clonarlo desde aquí.
Requerimientos
- Node.js 8.0+
- MySQL 5.7+
Primero instalaremos globalmente el módulo sequelize-cli:
npm i -g sequelize-cli
Después crearemos una carpeta donde contenga nuestra aplicación, crearemos un archivo js e instalaremos sequelize y su dialecto (en este caso MySQL):
mkdir howto-sequelize && cd howto-sequelize touch index.js npm init -y npm i -S sequelize mysql2
Ahora tenemos que iniciar el proyecto con sequelize-cli:
sequelize init
sequelize-cli nos creó una estructura base en la raíz de nuestro proyecto:
Si revisamos el archivo ./config/config.json, vemos que tenemos 3 opciones de conexión a una base de datos, modifiquemos la opción “development”:
"development": { "username": "root", "password": "tucontraseña", "database": "howto-sequelize", "host": "127.0.0.1", "dialect": "mysql", "operatorsAliases": false, "dialectOptions": { "charset": "utf8mb4" }, "logging": true, "benchmark": true }
Ahora revisemos el archivo ./models/index.js. Este archivo tiene como función crear una nueva instancia de Sequelize cada vez que sea llamado, y tiene como variable de entorno default a “development”, la cual utilizará la base de datos, host, usuario, contraseña y opciones que acabamos de agregar.
Creemos nuestra base de datos con el siguiente comando:
sequelize db:create
Muy bien! Ahora empecemos a crear nuestros modelos:
sequelize model:generate --name Usuario --attributes nombre:string,apellidoP:string,apellidoM:string,email:string sequelize model:generate --name LenguajeP --attributes nombre:string sequelize model:generate --name Usuario_LenguajeP --attributes UsuarioId:integer,LenguajePId:integer
Después de crear nuestros modelos hay que hacer la relación entre usuarios y lenguajes.
./models/usuario.js
'use strict'; module.exports = (sequelize, DataTypes) => { var Usuario = sequelize.define('Usuario', { nombre: DataTypes.STRING, apellidoP: DataTypes.STRING, apellidoM: DataTypes.STRING, email: DataTypes.STRING }, {}); Usuario.associate = function(models) { // associations can be defined here Usuario.belongsToMany(models.LenguajeP, { through: 'Usuario_LenguajeP', as: 'lenguajesProgramacion', foreignKey: 'UsuarioId', }) }; return Usuario; };
Analícemos esto, vemos que nuestro modelo Usuario ejecuta una función belongsToMany la cual apunta al modelo LenguajeP, en pocas palabras estamos indicando que un usuario puede pertenecer a varios lenguajes. En las opciones, through indica la tabla por la que tiene que cruzar para encontrar estas relaciones, as, es opcional, es el nombre de la propiedad o key que nos entregará estas relaciones, foreignKey marca por que columna queremos que busque estas relaciones.
./models/lenguajep.js
'use strict'; module.exports = (sequelize, DataTypes) => { var LenguajeP = sequelize.define('LenguajeP', { nombre: DataTypes.STRING }, {}); LenguajeP.associate = function (models) { // associations can be defined here LenguajeP.belongsToMany(models.Usuario, { through: 'Usuario_LenguajeP', as: 'usuarios', foreignKey: 'LenguajePId', }) }; return LenguajeP; };
Hacemos lo mismo con los lenguajes (LenguajeP), pero ahora apuntando al modelo Usuario.
Recordemos un poco lo que platicamos arriba, ORM trabaja sobre una capa que es el mapping (mapeo), estas relaciones sólo se verán efectuadas en el proyecto, nos falta crear una migración que afecte a la base de datos. Existen ORM’s que revisan si has hecho cambios a tus modelos y crean nuevas migraciones a partir de estos cambios (Django ORM, peewee), en nuestro caso Sequelize no cuenta con eso, así que nosotros crearemos nuestras migraciones:
sequelize migration:generate --name relation-many-to-many
Esto nos generó un archivo nuevo con un esqueleto en nuestras migraciones, ahora tenemos que modificarlo:
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return [ queryInterface.addConstraint('Usuario_LenguajeP', ['UsuarioId'], { type: 'FOREIGN KEY', name: 'FK_UsuarioLenguajeP_Usuario_1', references: { table: 'Usuario', field: 'id', }, onDelete: 'no action', onUpdate: 'no action', }), queryInterface.addConstraint('Usuario_LenguajeP', ['LenguajePId'], { type: 'FOREIGN KEY', name: 'FK_UsuarioLenguajeP_LenguajeP_1', references: { table: 'LenguajeP', field: 'id', }, onDelete: 'no action', onUpdate: 'no action', }), ] }, down: (queryInterface, Sequelize) => { return [ queryInterface.removeConstraint('Usuario_LenguajeP', 'FK_UsuarioLenguajeP_Usuario_1'), queryInterface.removeConstraint('Usuario_LenguajeP', 'FK_UsuarioLenguajeP_LenguajeP_1'), ] } };
Este archivo exporta un objeto con 2 propiedades, up y down. La propiedad up está encargada de entregar una promesa que altere los datos (crear tablas, relaciones, campos, cambiar tipos, etc), y la propiedad down hace lo contrario, revierte los cambios que se hayan efectuado en up.
Muy bien, ahora toca mi parte favorita, hay que correr los scripts de migración con el siguiente comando:
sequelize db:migrate
BOOM! Pero qué pasó?!?! Si leemos con atención el error en la consola dice que no encuentra la tabla Usuario_LenguajeP en la base de datos, revisemos las tablas. Hay algo curioso, todas terminan con una “s”, esto es porque sequelize-cli maneja las tablas en plural por default, aún cuando en nuestras opciones tengamos freezeTableName: true, este caso lo pueden ver aquí.
Entonces solo nos falta cambiar el nombre de las tablas a plural en relation-many-to-many (Usuario_LenguajePs, Usuarios, LenguajePs).
sequelize db:migrate:undo:all sequelize db:migrate
Las migraciones fueron exitosas! Ahora tenemos que poblar estas tablas, sequelize-cli utiliza las seeds (semillas). Vamos a crear 3 archivos seed:
sequelize seed:generate --name usuario sequelize seed:generate --name lenguajep sequelize seed:generate --name usuario_lenguajep
Estas semillas están ubicadas en la carpeta ./seeders, hay que poner los datos que deseamos cargar en la base de datos.
Semilla usuario:
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.bulkInsert('Usuarios', [ { nombre: 'John', apellidoP: 'Q', apellidoM: 'Doe', email: 'johndoe@gmail.com', createdAt: new Date(), updatedAt: new Date(), }, ], {}); }, down: (queryInterface, Sequelize) => { return queryInterface.bulkDelete('Usuarios', null, {}); } };
Semilla lenguajep:
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.bulkInsert('LenguajePs', [ { nombre: 'Java', createdAt: new Date(), updatedAt: new Date(), }, { nombre: 'JavaScript', createdAt: new Date(), updatedAt: new Date(), }, { nombre: 'PHP', createdAt: new Date(), updatedAt: new Date(), }, { nombre: 'Go', createdAt: new Date(), updatedAt: new Date(), }, { nombre: 'C++', createdAt: new Date(), updatedAt: new Date(), }, ], {}); }, down: (queryInterface, Sequelize) => { return queryInterface.bulkDelete('LenguajePs', null, {}); } };
Semilla usuario_lenguajep:
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.bulkInsert('Usuario_LenguajePs', [ { UsuarioId: 1, LenguajePId: 2, createdAt: new Date(), updatedAt: new Date(), }, { UsuarioId: 1, LenguajePId: 5, createdAt: new Date(), updatedAt: new Date(), }, ], {}); }, down: (queryInterface, Sequelize) => { return queryInterface.bulkDelete('Usuario_LenguajePs', null, {}); } };
Ahora hay que cargar estos datos a la base de datos con el siguiente comando:
sequelize db:seed:all
Muy bién! Ya vimos cómo inicializar un proyecto con sequelize, crear modelos, asignar relaciones, crear migraciones, correr y deshacer migraciones, crear semillas y cargarlas a la base de datos, solo nos falta hacer queries. Vamos a nuestro index.js en raíz y escribimos la siguiente función:
const db = require('./models'); const main = async () => { try { const usuarios = await db.Usuario.findAll({ include: [{ model: db.LenguajeP, as: 'lenguajesProgramacion', attributes: { exclude: ['createdAt', 'updatedAt'] }, through: { attributes: [] }, }], }); console.log(JSON.stringify(usuarios)); process.exit(); } catch (error) { console.log(error); } }; main();
Lo guardamos y ejecutamos el programa (ya sea por consola o con la herramienta de debug de visual studio code) y deberíamos recibir un JSON con una lista de usuarios (en este ejemplo solo existe uno) con sus datos y lenguajes:
[ { "id": 1, "nombre": "John", "apellidoP": "Q", "apellidoM": "Doe", "email": "johndoe@gmail.com", "createdAt": "2018-08-04T19:35:19.000Z", "updatedAt": "2018-08-04T19:35:19.000Z", "lenguajesProgramacion": [ { "id": 2, "nombre": "JavaScript" }, { "id": 5, "nombre": "C++" } ] } ]
Aprendimos sequelize-cli y lo básico de Sequelize, ahora podemos crear un API que ejecute acciones CRUD para que nuestros clientes la consuman!