sequelize

El acceso a bases de datos no está implementado dentro del núcleo de node.js ni de express, por lo que se precisa de middleware de terceros. sequelize es una herramienta ORM (Object–relational mapping), que es una técnica de programación para convertir datos entre sistemas de tipos incompatibles, usando lenguajes de programación orientados a objetos basada en promesas de node.js para Postgres, MySQL, MariaDB, SQLite y Microsoft SQL Server.

Para poder utilizar sequelize debemos instalarlo para el proyecto:

npm install sequelize    

Y se debe instalar también el paquete específico para el tipo de servidor de bases de datos que se vaya a utilizar, uno de los siguientes:

npm install  pg pg-hstore # Postgres
npm install  mysql2
npm install  mariadb
npm install  sqlite3
npm install  tedious # Microsoft SQL Server    

En adelante vamos a referirnos a MySQL. Para conectar con la base de datos debemos instanciar la clase Sequelize pasándole al constructor como argumento, bien una cadena con el URI para la conexión, o pasándole un objeto con los datos para realizar la conexión. En el ejemplo, conexión al propio equipo y a la base de datos videoClub con credenciales usuario node y contraseña 123456

const { Sequelize } = require('sequelize');

// Opción 1: URI de conexión
const sequelize = new Sequelize("mysql://node:123456@localhost/videoClub");

// Opción 2: Pasando el objeto con los datos de conexión
const sequelize = new Sequelize("videoClub", "node", "123456", {
    host: "localhost",
    dialect: "mysql",    
});    

Si se desea comprobar que la conexión se hizo de forma correcta, podemos invocar el método authenticate del objeto sequelize. El método authenticate devuelve una promesa, por lo tanto, podemos utilizar la sintaxis then … catch de las promesas o invocar mediante await, siempre que estemos en una función async. En el ejemplo siguiente se ven las dos versiones:

sequelize.authenticate().then(() => {
            console.log('Conectado')
        })
        .catch(err => {
            console.log('No se conectó');
        });

(async() => {
    try {
        await sequelize.authenticate();
        console.log('Conectado');
    } catch (error) {
        console.log('No se conectó');
    }
})();    

La conexión se puede cerrar invocando el método close del objeto sequelize. El método close devuelve una promesa como casi todos los métodos de la clase Sequelize.

Por defecto, Sequelize hace log a consola de todas las sentencias SQL que ejecuta. Si se quiere desactivar el logging se debe pasar la propiedad logging:false en los datos de la conexión:

const sequelize = new Sequelize("videoClub", "node", "123456", {
    host: "localhost",
    dialect: "mysql",   
    logging:false 
});    

En el desarrollo de aplicaciones, utilizando el paradigma MVC (modelo, vista, controlador), el modelo hace referencia a la descripción de los datos que se van a utilizar, representado generalmente a través de definición de clases. En Sequelize un modelo es una abstracción que representa una tabla de la base de datos. Es una clase que extiende la clase Model. El modelo le indica a Sequelize como se llama la tabla en la base de datos, los campos que contiene, el tipo de datos … El modelo tiene un nombre que no tiene por qué coincidir con el nombre de la tabla. Normalmente, el modelo tiene un nombre en singular, socio, mientras que las tablas tienen un nombre en plural en la base de datos, socios. Todo ello se puede configurar, aunque por defecto, el nombre para la tabla, cuando no se especifique, es el nombre del modelo en minúsculas y en plural. Por lo tanto, si nuestro modelo se llama Socio la tabla en la BBDD se presupone socios.

La definición del modelo se puede hacer de dos formas: una haciendo uso del método define del objeto sequelize y la otra extendiendo la clase Model del paquete sequelize. En el ejemplo que sigue se ve, de ambas formas, la definición del modelo que representa la tabla socios de una base de datos videoClub :

/ con método define
const { Sequelize,  DataTypes } = require("sequelize");
const sequelize = new Sequelize("mysql://node:123456@localhost/videoClub");

const Socio = sequelize. define("Socios", {
        ID: {
            type: DataTypes.INTEGER,
            primaryKey: true,
            allowNull: false,
            unique: true
        },
        nombre: {
            type: DataTypes.STRING(30)
        },
        apellidos: {
            type: DataTypes.STRING(50)
        },
        direccion: {
            type: DataTypes.STRING(100)
        },
        telefono: {
            type: DataTypes.STRING(9)
        },
        fechaNacimiento: {
            type: DataTypes.DATE
        },
        nif: {
            type: DataTypes.STRING(9)
        }
    });

// extendiendo la clase Model
     const { Sequelize, DataTypes, Model } = require("sequelize");
  const sequelize = new Sequelize("mysql://node:123456@localhost/videoClub");
    class Socio extends Model {}

    Socio.init({
        ID: {
            type: DataTypes.INTEGER,
            primaryKey: true,
            allowNull: false,
            unique: true
        },
        nombre: {
            type: DataTypes.STRING(30)
        },
        apellidos: {
            type: DataTypes.STRING(50)
        },
        direccion: {
            type: DataTypes.STRING(100)
        },
        telefono: {
            type: DataTypes.STRING(9)
        },
        fechaNacimiento: {
            type: DataTypes.DATE
        },
        nif: {
            type: DataTypes.STRING(9)
        }
    }, {
        // otras opciones para el modelo
         sequelize, // referencia a la conexión
         modelName: 'socios' // nombre del modelo
    });    

Si la única opción de la definición de un campo es la del tipo, esta se puede abreviar. Por ejemplo, el campo direccion del ejemplo anterior podría haber quedado definido:

    direccion: {DataTypes.STRING(100)}

Cuando se define el modelo, se le está diciendo a Sequelize como es la tabla en la base de datos, pero puede que en la base de datos la tabla a la que nos referimos no sea exactamente igual a la descrita en el modelo. Mediante el método sync del modelo podemos resolver estas diferencias.

Este método se invocará inmediatamente después de definir el modelo y es un método asíncrono que devuelve una promesa.

await Socio.sync();    

Por defecto, cuando Sequelize crea una tabla le añade dos campos adicionales a los especificados en el modelo, createdAt y updatedAt de tipo DATETIME cuyo contenido es gestionado automáticamente por Sequelize. El primero guarda la fecha y hora en la que la fila fue creada y el segundo la fecha y hora en que fue modificada por última vez.

La definición del modelo declara una clase. A partir de la clase debemos declarar instancias. La declaración de una instancia desde la clase del modelo no se hace con new, sino mediante la invocación del método build, que construye en memoria la instancia sin modificar nada en la tabla subyacente, por lo tanto, no es un método asíncrono. Una vez construida la instancia y modificado sus propiedades, para que estos se reflejen en la tabla subyacente, se debe invocar el método save que sí es asíncrono. El siguiente código crea una nueva fila y la guarda en la tabla.

try {
    const filaNueva = Socio.build({
        ID: 1,
        nombre: "Pedro",
        apellidos: "García Sánchez",
        direccion: "C/ La Encina 14",
        telefono: "923445566",
        fechaNacimiento: "1992/03/21",
        nif: "00000000T"
    });
    await filaNueva.save();
} catch (error) {
    console.log(error);
}    

La creación de una nueva fila se puede abreviar invocando el método asíncrono create que es lo mismo que hacer build y a continuación save.

try {
    const fileNueva = await Socio.create({
        ID: 1,
        nombre: "Pedro",
        apellidos: "García Sánchez",
        direccion: "C/ La Encina 14",
        telefono: "923445566",
        fechaNacimiento: "1992/03/21",
        nif: "00000000T"
    });
} catch (error) {
    console.log(error);
}
    

La modificación de los datos de una fila en la base de datos pasa primero por recuperar la fila en una instancia del modelo, por ejemplo, con el método findByPk que busca por clave principal, modificar sus propiedades, y guardar con save. Se puede abreviar con el método update, que es asíncrono.

try {
    const fila1 = await Socio.findByPk(1);
    fila1.nombre = "Jesús";
    await fila1.save();
} catch (error) {
    console.log(error);
};

try {
    const fila1 = await Socio.findByPk(1);
    await fila1.update({ nombre: "Ismael" });
} catch (error) {
    console.log(error);
}        
    

El método destroy permite eliminar filas de la tabla. Si se invoca sin argumentos y desde la instancia del modelo elimina la fila subyacente.


try {
    const fila1 = await Socio.findByPk(1);
    await fila1.destroy();
} catch (error) {
    console.log(error);
}    

Si se invoca desde el modelo se pueden eliminar una o varias filas sin necesidad de recuperarlas a instancias. El siguiente ejemplo borra todas las filas cuyo ID es superior a 2.

const { Sequelize, DataTypes, Model, Op } = require("sequelize");
const sequelize = new Sequelize("mysql://node:123456@localhost/videoClub");
:  
:
  try {
        await Socio.destroy({
            where: {
                ID: {
                    [Op.gte]: 2
                }
            }
        });
    } catch (error) {
        console.log(error);
    }

Véase como en require se importan los distintos tipos de clases que contiene el paquete sequelize. La cláusula where, que admiten varios métodos del objeto sequelize, admite un gran número de operandos para afinar la búsqueda.

El método findAll permite recuperar varias filas en base a un criterio formado en la cláusula where. El método devuelve un array con las filas recuperadas. Cada fila se corresponde con una instancia de la clase del modelo. El siguiente ejemplo muestra el ID y el nombre de los socios que tienen algún apellido Pérez.

try {
        const datos = await Socio.findAll({
            attributes: ["ID", "nombre"],
            where: {
                apellidos: {
                    [Op.like]: "%Pérez%"
                }
            }
        });
        console.log(JSON.stringify(datos));
    } catch (error) {
        console.log(error);
    }    

La opción attributes permite indicar cuales son los campos que se van a recuperar de cada fila que cumpla el criterio de búsqueda.

Se puede cambiar el nombre de una columna en la consulta encerrando entre corchetes el nombre en el modelo y el nombre nuevo en el resultado de la consulta:

try {
        const datos = await Socio.findAll({
            attributes: ["ID", ["nombre",”alias”]],
            where: {
                apellidos: {
                    [Op.like]: "%Pérez%"
                }
            }
        });
        console.log(JSON.stringify(datos));
    } catch (error) {
        console.log(error);
    }    

La columna nombre del modelo será conocida como alias en las filas devueltas. En el siguiente se ve un ejemplo de consulta con funciones de agregado. Se recupera el nombre y los apellidos con nombre filiación:

try {
        const datos = await Socio.findAll({
            attributes: ["ID",
                        [sequelize.fn("CONCAT", sequelize.col("nombre"), 
                                             ", ", sequelize.col("apellidos")), "filiacion"]],
            where: {
                apellidos: {
                    [Op.like]: "%Pérez%"
                }
            }
        });
        console.log(JSON.stringify(datos));
    } catch (error) {
        console.log(error);
    }     
 

El método fn nos permite invocar las funciones nativas del motor de base de datos.

Otro ejemplo:

try {

    const datos = await Socio.findAll({
        attributes: ["ID", "apellidos",
       [sequelize.fn('char_length', sequelize.col('apellidos')), "longitud"]],
    });
    console.log(JSON.stringify(datos));
} catch (error) {
    console.log(error);
}    

En donde se muestra la longitud de los apellidos.

En las consultas se puede especificar el orden de las filas devueltas especificando la opción order

try {
    const datos = await Socio.findAll({
        attributes: ["ID", "nombre", "apellidos"],
        order: [["apellidos", "DESC"],["nombre", "ASC"]]        
    });
    console.log(JSON.stringify(datos));
} catch (error) {
    console.log(error);
}    

El agrupamiento GROUP BY de SQL se codifica con la opción group:columna, y las opciones de paginación SQL, LIMIT y OFFSET se codifican con limit:numero y offset:numero.

Sequelize dispone además de los siguientes métodos count, max, min, sum, increment y decrement.

try {
    const datos = await Socio.count({
        where: { nombre: "Pedro" }
    });
    console.log(JSON.stringify(datos));
} catch (error) {
    console.log(error);
}    

Además de los métodos findAll y findByPk, Sequelize dispone de los siguientes métodos de búsqueda findOne, findOrCreate, findAndCountAll

Sequelize permite también definir getter y setter para los atributos del modelo. Por ejemplo, para el campo nif del modelo Socio se define un getter y un setter para impedir que se pueda introducir un nif incorrecto:

nif: {
    type: DataTypes.STRING(9),
     get() {
        return this.getDataValue("nif");
    },
    set(valor) {
        if (/^(\d{8})([A-HJ-NP-TV-Z])$/.test(valor) && 
                ("TRWAGMYFPDXBNJZSQVHLCKE" [(RegExp.$1 % 23)] == RegExp.$2))
            this.setDataValue("nif", valor)
        else
            throw "formato de nif incorrecto";

    }
}    

Se deben usar los métodos getDataValue y setDataValue para acceder a los valores subyacentes del modelo.

También se pueden definir campos virtuales (type: DataTypes.VIRTUAL) en el modelo que no tienen su respaldo directo en la tabla subyacente. Por ejemplo, se podría definir un campo nombreYApellidos que contuviera el contenido de los campos nombre y apellidos juntos

nombreYApellidos: {
    type: DataTypes.VIRTUAL,
     get() {
        return this.getDataValue("apellidos") + ", " + this.getDataValue("nombre")
    },
    set(valor) {
        throw "campo de solo lectura"
    }
},    

La validación de los contenidos de los campos antes de su almacenamiento en la tabla subyacente, ya se ha visto, se puede hacer en el setter del mismo, aunque Sequelize permite hacer la validación mediante la opción validate cuando se describe el campo en el modelo. Por ejemplo, si quisiéramos que el nombre solo contuviera letras:

nombre: {
    type: DataTypes.STRING(30),
     validate: { isAlpha: true }
},    

Hay multitud de validadores para los contenidos más comunes, para email, para fechas, para direcciones IP, para tarjetas de crédito, que se ajuste a una expresión regular, incluso se pueden definir funciones que devuelvan un error si el dato no es válido. Por ejemplo, si tenemos un campo codigoPostal y queremos que su contenido sean 5 dígitos

codigoPostal: {
    type: DataTypes.STRING(5),
    validate: { is: /^[0-9]{5}$/ }
},    

El ejemplo anterior en el que en el campo nif se validaba que fuera correcto en el setter, se podría codificar también:

nif: {
    type: DataTypes.STRING(9),
    validate: {
         isNif(valor) {
            if (!(/^(\d{8})([A-HJ-NP-TV-Z])$/.test(valor) &&
                ("TRWAGMYFPDXBNJZSQVHLCKE" [(RegExp.$1 % 23)] == RegExp.$2)))
                throw "formato de nif incorrecto";
        }
    }
}    

Sequelize permite la ejecución de código SQL directamente sobre la base de datos, formato raw, a través del método query del objeto instancia sequelize. El método query devuelve dos argumentos: el array con los resultados y un objeto con los metadatos como por ejemplo el número de filas afectadas por la ejecución de la sentencia SQL.

try {
    const [datos, metadatos] = await sequelize.query("UPDATE socios 
                                        SET nif='00000000T' WHERE ID=1;");
    console.log("datos:" + JSON.stringify(datos));
    console.log("metadatos:" + JSON.stringify(metadatos));
} catch (error) {
    console.log(error);
}    

Si probamos el ejemplo anterior veremos que tanto datos como metadatos tienen el mismo contenido. Lo mismo ocurre si ejecutamos el siguiente código:

try {
    const [datos, metadatos] = 
                    await sequelize.query("SELECT * FROM socios ORDER BY nombre;");
    console.log(JSON.stringify(datos));
    console.log("metadatos:" + JSON.stringify(metadatos));
} catch (error) {
    console.log(error);
}    

Para evitar poner los dos argumentos que devuelve el método query podemos poner el tipo de consulta que se va a ejecutar y prescindir del argumento que no nos interesa. Por ejemplo:

const { Sequelize, DataTypes, QueryTypes, Model, Op } = require("sequelize");

try {
    const datos = await sequelize.query("SELECT * FROM socios ORDER BY nombre;",
                                            { type: QueryTypes.SELECT });
                                                console.log(JSON.stringify(datos));
                                            } catch (error) {
                                                console.log(error);
                                            }    

Podemos indicar el modelo para los datos devueltos:

const { Sequelize, DataTypes, QueryTypes, Model, Op } = require("sequelize");

    try {
        const datos = await sequelize.query("SELECT * FROM socios ORDER BY nombre;",
                        { type: QueryTypes.SELECT,
                           modelName: Socio });
        console.log(JSON.stringify(datos));
    } catch (error) {
        console.log(error);
    }      
  

datos es ahora un array de instancias del modelo.

En las consultas SQL con parámetros se utiliza el carácter ? para indicar la posición del mismo y luego se enumeran de forma separada para ocupar posicionalmente los lugares donde se especificó el carácter ?. En el método query se puede especificar la lista de parámetros con la opción replacements. En el siguiente ejemplo se ve como se utiliza:

const datos = await sequelize.query("SELECT * FROM socios WHERE ID > ? OR nombre>?;", 
                                    { type: QueryTypes.SELECT, 
                                      modelName: Socio, 
                                      replacements: [2, "M"] });
console.log(JSON.stringify(datos));
} catch (error) {
    console.log(error);
}    

También se permiten los parámetros con nombre. Se identifican porque llevan : delante del nombre del parámetro. El anterior ejemplo se podría haber codificado:

try {
    const datos = await sequelize.query(
                        "SELECT * FROM socios WHERE ID > :id OR nombre>:letra;", 
                        { type: QueryTypes.SELECT, 
                          modelName: Socio,
                          replacements: { id: 2, letra: "M" } });
    console.log(JSON.stringify(datos));
} catch (error) {
    console.log(error);
}    

Nótese, que si utilizan las consultas raw, se pierde la funcionalidad de utilización de validadores de contenido de forma automática y se debe hacer de forma manual la trasferencia desde el modelo a la consulta raw, o prescindir del modelo y componer la consulta de forma manual.

Es recomendable, en cuanto a eficiencia, la utilización de procedimientos almacenados en el servidor, que ya están compilados, en lugar de solicitar la operación con la sentencia SQL como texto que debe ser interpretada y ejecutar, facilitando además la inserción de código SQL dañino (SQL injection attack), pero Sequelize no tiene un método especifico para la ejecución de procedimientos almacenados.

Por ejemplo, si existiera en la base de datos un procedimiento almacenado como el que sigue:

CREATE PROCEDURE `listaSociosPorIdyLetra`(IN numero INTEGER , 
                                          IN letra VARCHAR(15))
BEGIN
   SELECT * FROM socios WHERE ID>numero OR nombre>letra;
END    

El ejemplo anterior quedaría codificado:

try {
        const datos = await sequelize.query("call listaSociosPorIdyLetra(:id,:letra);",
                                    { type: QueryTypes.SELECT, 
                                      modelName: Socio, 
                                      replacements: { id: 2, letra: "M" } });
        console.log(JSON.stringify(datos));
    } catch (error) {
        console.log(error);
    }    

Por último indicar que Sequelize tiene soporte para transacciones, asi como soporte para especificar relaciones entre tablas y mantenimiento de la integridad referencial.

Almacén de sesiones en mySQL con sequelize

En la propiedad store del middleware express-session, podíamos indicar el almacén en donde guardar la información de las sesiones de los clientes. Por defecto es MemoryStore pero se puede indicar como almacén casi cualquier servidor de base de datos, por ejemplo, en mySQL. Para hacerlo fácilmente disponemos del middleware express-session-sequelize .

El propio middleware creará, por defecto, la tabla Sessions en la base de datos a la que se conecte para almacenar en su interior la información de las sesiones. Se necesita también la importación del middleware cookie-parser.

En el siguiente ejemplo se ve un caso de uso del middleware. El ejemplo es el mismo que se vio en la explicación de las sesiones, que contaba el número de visitas durante la sesión. En este caso se hace funcionar el servidor en modo https con certificado y como almacén de la información de las sesiones se utiliza la tabla Sessions en la base de datos videoClub que son creadas si no existieran.

const cookieParser = require('cookie-parser');
const express = require('express');
const sesion = require('express-session');
const app = express();

const fs = require("fs");
const https = require("https");

const Sequelize = require("sequelize");
const SessionStore = require('express-session-sequelize')(sesion.Store);


(async() => {
    try {
        const sequelize = new Sequelize("videoClub", "node", "123456", {
            host: "localhost",
            dialect: "mysql",
            logging: false
        });

        app.use(cookieParser());

        app.use(sesion({
            cookie: {
                signed: true,
                secure: true
            },
            secret: "a3A$d23jU@",
            name: "chj",
            resave: true,
            saveUninitialized: true,
            store: new SessionStore({
                db: sequelize,
            })
        }));

        app.get("/", (req, res) => {
            req.session.veces = (req.session.veces) ? (req.session.veces + 1) : 1;
            res.send("veces: " + req.session.veces);
        });

        app.use((req, res, next) => {
            res.send(404, "recurso no encontrado"); // termina
        });

        https.createServer({
            key: fs.readFileSync('certificado.key'),
            cert: fs.readFileSync('certificado.crt')
        }, app).listen(8080, function() {
            console.log("servidor HTTPS funcionando en puerto 8080 ...");
        });

    } catch (error) {
        console.log("error en acceso a la base de datos: " + error);
    }
})();    
e-mail:manjarrés