Usando Sequelize ORM con Node.js y Express

Sequelize es un ORM popular y estable que se usa con Node.js. En este artículo, configuraremos una aplicación de administración de notas que realiza operaciones CRUD en la base de datos mediante Sequelize.

Introducción

Secuela es un ORM popular creado para Node.js, y en este tutorial lo usaremos para crear una API CRUD para administrar notas.

La interacción con las bases de datos es una tarea común para las aplicaciones de back-end. Por lo general, esto se hacía a través de consultas SQL sin formato, que pueden ser difíciles de construir, especialmente para aquellos que son nuevos en SQL o en bases de datos en general.

Finalmente, surgieron Object Relational Mappers (ORM), diseñados para facilitar la administración de bases de datos. Mapean automáticamente los objetos (entidades) de nuestro código en una base de datos relacional, como su nombre lo indica.

Ya no escribiríamos consultas SQL sin procesar y las ejecutaríamos contra la base de datos. Al proporcionarnos una forma programática de conectar nuestro código a la base de datos y manipular los datos persistentes, podemos centrarnos más en la lógica empresarial y menos en SQL propenso a errores.

¿Qué es un ORM?

Mapeo relacional de objetos es una técnica que mapea objetos de software a tablas de bases de datos. Los desarrolladores pueden interactuar con objetos en lugar de tener que escribir consultas de base de datos. Cuando se lee, crea, actualiza o elimina un objeto, el ORM construye y ejecuta una consulta de base de datos bajo el capó.

Otra ventaja de los ORM es que admiten varias bases de datos: postgres, mysql, [SQLite](https:// www.sqlite.org/index.html), etc. Si escribe una aplicación utilizando consultas sin formato, será difícil pasar a una base de datos diferente porque muchas de las consultas deberán volver a escribirse.

Con un ORM, el propio ORM realiza el cambio de bases de datos y, por lo general, todo lo que necesita hacer es cambiar uno o dos valores en un archivo de configuración.

Secuela

Hay muchos ORM de Nodo, incluidos los populares Estantería.js y TipoORM.

Entonces, ¿por qué y cuándo elegir Sequelize?

En primer lugar, existe desde hace mucho tiempo: 2011. Tiene miles de estrellas de GitHub y es utilizado por toneladas de aplicaciones. Debido a su antigüedad y popularidad, es estable y tiene mucha documentación disponible en línea.

Además de su madurez y estabilidad, Sequelize tiene un gran conjunto de características que cubre: consultas, ámbitos, relaciones, transacciones, consultas sin procesar, migraciones, replicación de lectura, etc.

Una cosa a tener en cuenta es que Sequelize se basa en promesas, lo que facilita la administración de funciones asincrónicas y excepciones. También es compatible con todos los dialectos populares de SQL: PostgreSQL, MySQL, MariaDB, SQLite y MSSQL.

Por otro lado, no hay compatibilidad con NoSQL que se pueda ver en los ORM (u Object Document Mappers, en este caso) como Mangosta. Realmente, decidir qué ORM elegir depende principalmente de los requisitos del proyecto en el que está trabajando.

Instalación de Sequelize

Nota: Si quieres seguir el código, puedes encontrarlo aquí en GitHub.

Hagamos una aplicación de nodo esqueleto e instalemos Sequelize. En primer lugar, creemos un directorio para nuestro proyecto, introdúzcalo y cree un proyecto con la configuración predeterminada:

1
2
3
$ mkdir notes-app
$ cd notes-app
$ npm init -y

A continuación, crearemos el archivo de la aplicación con un servidor Express básico y un enrutador. Llamémoslo index.js para que coincida con el nombre de archivo predeterminado de npm init:

A continuación, para crear fácilmente un servidor web, instalaremos Express:

1
$ npm install --save express

Y con él instalado, configuremos el servidor:

1
2
3
4
5
6
7
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Notes App'));

app.listen(port, () => console.log(`notes-app listening on port ${port}!`));

Finalmente, podemos seguir adelante e instalar Sequelize y nuestra base de datos de elección a través de npm:

1
2
$ npm install --save sequelize
$ npm install --save sqlite3

No importa qué base de datos utilice, ya que Sequelize es independiente de la base de datos. La forma en que lo usamos es la misma, sin importar la base de datos subyacente. Es fácil trabajar con SQLite3 para el desarrollo local y es una opción popular para esos fines.

Ahora, agreguemos algo de código al archivo index.js para configurar la base de datos y verificar la conexión usando Sequelize. Dependiendo de la base de datos que esté utilizando, es posible que deba definir un dialecto diferente:

1
2
3
4
5
6
7
const Sequelize = require('sequelize');
const sequelize = new Sequelize({
  // The `host` parameter is required for other databases
  // host: 'localhost'
  dialect: 'sqlite',
  storage: './database.sqlite'
});

Después de importar Sequelize, lo configuramos con los parámetros que requiere para ejecutarse. También puede agregar más parámetros aquí, como el grupo, aunque lo que tenemos es suficiente para comenzar. El dialecto depende de la base de datos que esté utilizando, y el almacenamiento simplemente apunta al archivo de la base de datos.

El archivo database.sqlite se crea automáticamente en el nivel raíz de nuestro proyecto.

Nota: Vale la pena revisar la Secuela de documentos para configurar diferentes bases de datos y la información requerida para cada una.

Si está utilizando MySQL, Postgres, MariaDB o MSSQL, en lugar de pasar cada parámetro por separado, también puede pasar el URI de conexión:

1
const sequelize = new Sequelize('postgres://user:[correo electrónico protegido]:5432/dbname');

Finalmente, probemos la conexión ejecutando el método .authenticate(). Bajo el capó, simplemente ejecuta una consulta SELECT y verifica si la base de datos responde correctamente:

1
2
3
4
5
6
7
8
sequelize
  .authenticate()
  .then(() => {
    console.log('Connection has been established successfully.');
  })
  .catch(err => {
    console.error('Unable to connect to the database:', err);
  });

Ejecutando la aplicación, somos recibidos con:

1
2
3
4
$ node index.js
notes-app listening on port 3000!
Executing (default): SELECT 1+1 AS result
Connection has been established successfully.

Creación de un modelo para mapeo

Antes de que podamos construir una API de notas, necesitamos crear una tabla de notas. Para hacer eso, necesitamos definir un modelo Nota, que asignaremos a una constante para que pueda usarse en toda nuestra API. En la función define especificamos el nombre de la tabla y los campos. En este caso, un campo de texto para la nota y una cadena para la etiqueta:

Al igual que con las bases de datos relacionales, antes de crear una API, primero debemos crear las tablas adecuadas. Ya que queremos evitar crearlo a mano usando SQL, definiremos una clase Modelo y luego haremos que Sequelize lo asigne a una tabla.

Esto se puede hacer extendiendo la clase Sequelize.Model y ejecutando la función .init(), pasando parámetros, o definiendo una const y asignándole el valor devuelto del método .define(). de Secuela.

El último es más conciso, así que nos quedaremos con ese:

1
const Note = sequelize.define('notes', { note: Sequelize.TEXT, tag: Sequelize.STRING });

Mapeo del modelo a la base de datos

Ahora que tenemos un modelo Nota podemos crear la tabla notas en la base de datos. En una aplicación de producción, normalmente haríamos cambios en la base de datos a través de [migraciones] (https://sequelize.org/v5/manual/migrations.html) para que los cambios se rastreen en el control de código fuente.

Sin embargo, para mantener las cosas concisas, usaremos el método .sync(). Lo que hace .sync() es simple: sincroniza todos los modelos definidos con la base de datos:

1
2
3
4
sequelize.sync({ force: true })
  .then(() => {
    console.log(`Database & tables created!`);
  });

Aquí, hemos usado el indicador forzar y lo configuramos como verdadero. Si una tabla ya existe, el método la ‘BOTARÁ’ y ‘CREARÁ’ una nueva. Si no existe, simplemente se crea una tabla.

Finalmente, creemos algunas notas de muestra que luego persistiremos en la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sequelize.sync({ force: true })
  .then(() => {
    console.log(`Database & tables created!`);

    Note.bulkCreate([
      { note: 'pick up some bread after work', tag: 'shopping' },
      { note: 'remember to write up meeting notes', tag: 'work' },
      { note: 'learn how to use node orm', tag: 'work' }
    ]).then(function() {
      return Note.findAll();
    }).then(function(notes) {
      console.log(notes);
    });
  });

Al ejecutar el servidor, nuestras notas se imprimen en la consola, así como las operaciones SQL realizadas por Sequelize. Conectémonos a la base de datos para verificar que los registros se hayan agregado correctamente:

1
2
3
4
5
6
$ sqlite3 database.sqlite
sqlite> select * from notes;
1|pick up some bread after work|shopping|2020-02-21 18:24:19.402 +00:00|2020-02-21 18:24:19.402 +00:00
2|remember to write up meeting notes|work|2020-02-21 18:24:19.402 +00:00|2020-02-21 18:24:19.402 +00:00
3|learn how to use node orm|work|2020-02-21 18:24:19.402 +00:00|2020-02-21 18:24:19.402 +00:00
sqlite> .exit

Con la base de datos en su lugar y nuestra(s) tabla(s) creada(s), avancemos e implementemos la funcionalidad CRUD básica.

Entidades de lectura

Nuestro modelo, Nota, ahora tiene métodos integrados que nos ayudan a realizar operaciones en los registros persistentes en la base de datos.

Leer todas las entidades

Por ejemplo, podemos leer todos los registros de esa clase guardados usando el método .findAll(). Hagamos un punto final simple que sirva a todas las entidades persistentes:

1
2
3
app.get('/notes', function(req, res) {
  Note.findAll().then(notes => res.json(notes));
});

El método .findAll() devuelve una matriz de notas, que podemos usar para representar un cuerpo de respuesta, a través de res.json.

Probemos el punto final a través de curl:

1
2
$ curl http://localhost:3000/notes
[{"id":1,"note":"pick up some bread after work","tag":"shopping","createdAt":"2020-02-27T17:02:10.881Z","updatedAt":"2020-02-27T17:02:10.881Z"},{"id":2,"note":"remember to write up meeting notes","tag":"work","createdAt":"2020-02-27T17:02:10.881Z","updatedAt":"2020-02-27T17:02:10.881Z"},{"id":3,"note":"learn how to use node orm","tag":"work","createdAt":"2020-02-27T17:02:10.881Z","updatedAt":"2020-02-27T17:02:10.881Z"}]

Como puede ver, todas las entradas de nuestra base de datos nos fueron devueltas, pero en formato JSON.

Sin embargo, si estamos buscando agregar un poco más de funcionalidad, tenemos operaciones de consulta como SELECT, WHERE, AND, OR y LIMIT compatibles con este método.

Puede encontrar una lista completa de los métodos de consulta admitidos en la página Secuela de documentos.

Entidades de lectura DONDE

Con eso en mente, hagamos un punto final que sirva una nota única y específica:

1
2
3
app.get('/notes/:id', function(req, res) {
  Note.findAll({ where: { id: req.params.id } }).then(notes => res.json(notes));
});

Los puntos finales aceptan un parámetro id, que se utiliza para buscar una nota a través de la cláusula WHERE. Vamos a probarlo a través de curl:

1
2
$ curl http://localhost:3000/notes/2
[{"id":2,"note":"remember to write up meeting notes","tag":"work","createdAt":"2020-02-27T17:03:17.592Z","updatedAt":"2020-02-27T17:03:17.592Z"}]

Nota: Dado que esta ruta usa un parámetro comodín, :id, coincidirá con cualquier cadena que venga después de /notas/. Por esta razón, esta ruta debe estar al final de su archivo index.js. Esto permite que otras rutas, como /notes/search, manejen una solicitud antes de que /notes/:id la recoja. De lo contrario, la palabra clave “buscar” en la ruta de la URL se tratará como una ID.

Entidades de lectura DONDE Y

Para consultas aún más específicas, hagamos un punto final utilizando las declaraciones ‘WHERE’ y ‘AND’:

1
2
3
app.get('/notes/search', function(req, res) {
  Note.findAll({ where: { note: req.query.note, tag: req.query.tag } }).then(notes => res.json(notes));
});

Aquí, buscamos notas que coincidan tanto con la nota como con la etiqueta especificadas por los parámetros. Nuevamente, probemos a través de curl:

1
2
$ curl "http://localhost:3000/notes/search?note=pick%20up%20some%20bread%20after%20work&tag=shopping"
[{"id":1,"note":"pick up some bread after work","tag":"shopping","createdAt":"2020-02-27T17:09:53.964Z","updatedAt":"2020-02-27T17:09:53.964Z"}]

Entidades de lectura O

Si estamos tratando de ser un poco más vagos, podemos usar la instrucción OR y buscar notas que coincidan con cualquier de los parámetros dados. Cambia la ruta /notes/search a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const Op = Sequelize.Op;

app.get('/notes/search', function(req, res) {
  Note.findAll({
    where: {
      tag: {
        [Op.or]: [].concat(req.query.tag)
      }
    }
  }).then(notes => res.json(notes));
});

Aquí estamos usando Sequelize.Op para implementar una consulta OR. Sequelize proporciona varios operadores para elegir, como Op.or, Op.and, Op.eq, Op.ne, Op.is, Op.not, etc. Estos son principalmente se utiliza para crear operaciones más complejas, como consultar con una cadena regex.

Tenga en cuenta que estamos usando req.query.tag como argumento para .findAll(). Sequelize espera una matriz aquí, por lo que forzamos que tag sea una matriz usando [].concat(). En nuestra prueba a continuación, pasaremos múltiples argumentos en nuestra URL de solicitud:

1
2
$ curl "http://localhost:3000/notes/search?tag=shopping&tag=work"
[{"id":1,"note":"pick up some bread after work","tag":"shopping","createdAt":"2020-02-27T17:11:27.518Z","updatedAt":"2020-02-27T17:11:27.518Z"},{"id":2,"note":"remember to write up meeting notes","tag":"work","createdAt":"2020-02-27T17:11:27.518Z","updatedAt":"2020-02-27T17:11:27.518Z"},{"id":3,"note":"learn how to use node orm","tag":"work","createdAt":"2020-02-27T17:11:27.518Z","updatedAt":"2020-02-27T17:11:27.518Z"}]

Al pasar el mismo parámetro de consulta varias veces de esta manera, se mostrará como una matriz en el objeto req.query. Entonces, en el ejemplo anterior, req.query.tag es ['shopping', 'work'].

LÍMITE de entidades de lectura

Lo último que cubriremos en esta sección es LIMIT. Digamos que queríamos modificar la consulta anterior para que solo devuelva dos resultados como máximo. Haremos esto agregando el parámetro limit y asignándole un número entero positivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const Op = Sequelize.Op;

app.get('/notes/search', function(req, res) {
  Note.findAll({
    limit: 2,
    where: {
      tag: {
        [Op.or]: [].concat(req.query.tag)
      }
    }
  }).then(notes => res.json(notes));
});

Puede ver una lista completa de funciones de consulta en la Secuela de documentos.

Inserción de entidades

Insertar entidades es mucho más sencillo ya que realmente no hay dos formas de realizar esta operación.

Agreguemos un nuevo punto final para agregar notas:

1
2
3
4
5
6
7
8
const bodyParser = require('body-parser');
app.use(bodyParser.json());

app.post('/notes', function(req, res) {
  Note.create({ note: req.body.note, tag: req.body.tag }).then(function(note) {
    res.json(note);
  });
});

Se requiere el módulo body-parser para que el punto final acepte y analice los parámetros JSON. No necesita instalar explícitamente el paquete body-parser porque ya está incluido con Express.

Dentro de la ruta estamos usando el método .create() para insertar una nota en la base de datos, según los parámetros pasados.

Podemos probarlo con otra solicitud curl:

1
2
$ curl -d '{"note":"go the gym","tag":"health"}' -H "Content-Type: application/json" -X POST http://localhost:3000/notes
{"id":4,"note":"go the gym","tag":"health","updatedAt":"2020-02-27T17:13:42.281Z","createdAt":"2020-02-27T17:13:42.281Z"}

La ejecución de esta solicitud dará como resultado la creación de una nota en nuestra base de datos y nos devolverá el nuevo objeto de la base de datos.

Actualizar entidades

A veces, desearíamos actualizar entidades ya existentes. Para hacer esto, confiaremos en el método .update() en el resultado del método .findByPk():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app.put('/notes/:id', function(req, res) {
  Note.findByPk(req.params.id).then(function(note) {
    note.update({
      note: req.body.note,
      tag: req.body.tag
    }).then((note) => {
      res.json(note);
    });
  });
});

El método .findByPk() también es un método heredado en nuestra clase modelo. Busca una entidad con la clave principal dada. Esencialmente, es más fácil devolver entidades individuales por su ID usando este método que escribir una consulta SELECCIONAR DONDE.

Dada la entidad devuelta, ejecutamos el método .update() para colocar los nuevos valores en su lugar. Verifiquemos esto a través de curl:

1
2
$ curl -X PUT -H "Content-Type: application/json" -d '{"note":"pick up some milk after work","tag":"shopping"}' http://localhost:3000/notes/1
{"id":1,"note":"pick up some milk after work","tag":"shopping","createdAt":"2020-02-27T17:14:55.621Z","updatedAt":"2020-02-27T17:14:58.230Z"}

Disparar esta solicitud actualiza la primera nota con nuevo contenido y devuelve el objeto actualizado:

Eliminación de entidades

Y finalmente, cuando nos gustaría eliminar registros de nuestra base de datos, usamos el método .destroy() en el resultado del método .findByPk():

1
2
3
4
5
6
7
app.delete('/notes/:id', function(req, res) {
  Note.findByPk(req.params.id).then(function(note) {
    note.destroy();
  }).then((note) => {
    res.sendStatus(200);
  });
});

La ruta para .delete() se parece a .update(). Usamos .findByPk() para encontrar una nota específica por ID. Luego, el método .destroy() elimina la nota de la base de datos.

Finalmente, se devuelve una respuesta 200 OK al cliente.

Conclusión

Mapeo relacional de objetos (ORM) es una técnica que mapea objetos de software a tablas de bases de datos. Sequelize es una herramienta ORM popular y estable que se utiliza junto con Node.js. En este artículo, hemos discutido qué son los ORM, cómo funcionan y cuáles son algunas de las ventajas de usarlos en lugar de escribir consultas sin formato.

Con ese conocimiento, procedimos a escribir una aplicación Node.js/Express simple que usa Sequelize para conservar un modelo Note en la base de datos. Usando los métodos heredados, hemos realizado operaciones CRUD en la base de datos.

No dude en consultar el código en GitHub si tuvo algún problema para seguir este tutorial. ial.