Node.js

Acceso a MySQL desde Node

     

En este documento veremos cómo acceder a bases de datos relacionales (en concreto, sistemas MySQL) desde Node.js. Explicaremos qué módulo vamos a utilizar para ello, y cómo realizar con él las operaciones básicas: conectar a la base de datos, operaciones de inserción/borrado/modificación y consultas o listados.

1. La librería mysql2

Conviene tener presente que la combinación de Node.js y MySQL no es demasiado habitual. Es bastante más frecuente el uso de bases de datos MongoDB empleando este framework, ya que la información que se maneja, en formato BSON, es muy fácilmente exportable entre los dos extremos.

Sin embargo, también existen herramientas para poder trabajar con MySQL desde Node.js. Una de las más populares es la librería mysql2, disponible en el repositorio NPM.

NOTA: existe una versión anterior de la librería, llamada mysql. Sin embargo, el mecanismo de autenticación de las últimas versiones de MySQL Server no es compatible con esta librería, por lo que es más recomendable emplear esta última versión.

Para ver cómo utilizarla, comenzaremos por crear un proyecto llamado “ContactosMySQL” en nuestra carpeta de “ProyectosNode/Pruebas”. Dentro crea dentro un archivo index.js, y ejecuta el comando npm init para inicializar el archivo package.json. Después, instalamos la librería con el correspondiente comando npm install:

npm install mysql2

2. Conexión a la base de datos (una sola conexión)

Una vez instalado el módulo, en nuestro archivo index.js lo importamos (con require), y ejecutamos el método createConnection para establecer una conexión con la base de datos, de acuerdo a los parámetros de conexión que facilitaremos en el propio método:

Para las pruebas que haremos en este proyecto de prueba, utilizaremos una base de datos llamada “contactos” que puedes descargar, descomprimir e importar desde aquí, aunque ya lo hemos pedido en un ejercicio previo. Teniendo en cuenta todo lo anterior, podemos dejar los parámetros de conexión así:

const mysql = require('mysql2');

// Cambiar usuario y contraseña por los que tengamos en
// nuestro sistema
let conexion = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "root",
    database: "contactos"
});

Después podemos establecer la conexión con:

conexion.connect((error) => {
    if (error)
        console.log("Error al conectar con la BD:", err);
    else
        console.log("Conexión satisfactoria");
});

En el caso de que se produzca algún error de conexión, lo identificaremos en el parámetro error y podremos actuar en consecuencia. En este caso se muestra un simple mensaje por la consola, pero también podemos almacenarlo en algún flag booleano o algo similar para impedir que se hagan operaciones contra la base de datos, o se redirija a otra página.

2.1 Pool de conexiones

En aplicaciones reales (APIs, servidores web, etc.) no es habitual trabajar con una única conexión a la base de datos, sino con un pool de conexiones.

Un pool es un conjunto de conexiones abiertas que la aplicación reutiliza según las necesidades. De este modo se evita el coste de abrir y cerrar conexiones continuamente y se mejora el rendimiento cuando hay varias peticiones simultáneas.

Para crear un pool de conexiones se utiliza el método createPool:

const mysql = require('mysql2');

const pool = mysql.createPool({
    host: "localhost",
    user: "root",
    password: "root",
    database: "contactos",
    connectionLimit: 10,
    waitForConnections: true,
    queueLimit: 0,
    charset: "utf8mb4"
});

Los parámetros de conexión básicos (host, user, password, database) son los mismos que en createConnection. Además, en el caso del pool, se pueden especificar los siguientes parámetros adicionales:

Una vez creado el pool, no es necesario llamar explícitamente a connect, ya que el propio pool se encarga de abrir y gestionar las conexiones de forma automática. El uso del pool se verá en los apartados siguientes, cuando se realicen consultas y operaciones sobre la base de datos.

3. Consultas

Una vez establecida la conexión con la base de datos (o creado el pool de conexiones), ya podemos realizar consultas SQL sobre la base de datos.

La base de datos “contactos” tiene una tabla del mismo nombre, con los atributos id, nombre y telefono.

Para ejecutar consultas se utiliza el método query, que está disponible tanto en una conexión individual como en un pool.

3.1 Consulta simple

Vamos a definir una consulta para obtener resultados y recorrerlos. Por ejemplo, mostrar todos los contactos:

conexion.query("SELECT * FROM contactos", 
(error, resultado, campos) => {
    if (error)
        console.log("Error al procesar la consulta");
    else 
    {
        resultado.forEach((contacto) => {
            console.log(contacto.nombre, ":",
                contacto.telefono);
        });
    }
});

Notar que el método query tiene dos parámetros: la consulta a realizar, y un callback que recibe otros tres parámetros: el error producido (si lo hay), el conjunto de resultados (que se puede procesar como un vector de objetos), e información adicional sobre los campos de la consulta.

Notar también que el propio método query nos sirve para conectar (dispone de su propio control de error), por lo que no sería necesario el paso previo del método connect. En cualquier caso, podemos hacerlo si queremos asegurarnos de que hay conexión, pero cada query que hagamos también lo puede verificar.

3.2 Consultas con parámetros

Cuando una consulta necesita utilizar valores variables, no se deben concatenar directamente en el SQL, ya que esto puede provocar problemas de seguridad (inyección SQL).

Para ello se utilizan placeholders, representados por el símbolo ?, que se sustituyen por los valores indicados en un array de parámetros que se coloca en segunda posición. Por ejemplo:

conexion.query("SELECT * FROM contactos WHERE id = ?", [1], 
    (error, resultado, campos) => {
        ...

Esta técnica separa el código SQL de los datos y evita que el contenido de los parámetros se interprete como parte de la consulta.

3.3 Consulta mediante objeto de configuración

El método querytambién admite un objeto como primer parámetro con las diferentes propiedades de la consulta: la instrucción SQL en sí, los parámetros embebidos mediante placeholders… de forma que podemos proporcionar información adicional como timeout, conversión de tipos, etc.

conexion.query({
    sql: "SELECT * FROM contactos WHERE id = ?",
    values: [1], 
    timeout: 4000
}, (error, resultado, campos) => {
    ...

Este formato resulta útil cuando se necesita configurar aspectos como tiempos de espera u otras opciones avanzadas.

4. Actualizaciones (inserciones, borrados, modificaciones)

Si lo que queremos es realizar alguna modificación sobre los contenidos de la base de datos (INSERT, UPDATE o DELETE), estas operaciones se realizan desde el mismo método query visto antes. La diferencia está en que en el parámetro resultado del callback ya no están los registros de la consulta, sino datos como el número de filas afectadas (en el atributo affectedRows), o el id del nuevo elemento insertado (atributo insertId), en el caso de inserciones con id autonumérico.

Por ejemplo, si queremos insertar un nuevo contacto en la agenda y obtener el id que se le ha asignado, lo podemos hacer así:

conexion.query("INSERT INTO contactos" + 
"(nombre, telefono) VALUES " +
"('Fernando', '966566556')", (error, resultado, campos) => {
    if (error)
        console.log("Error al procesar la inserción");
    else
        console.log("Nuevo id = ", resultado.insertId);
});

También podemos pasar un objeto JavaScript como dato a la consulta, y automáticamente se asigna cada campo del objeto al campo correspondiente de la base de datos (siempre que los nombres de los campos coincidan). Esto puede emplearse tanto en inserciones como en modificaciones:

conexion.query("INSERT INTO contactos SET ?", 
    {nombre: 'Nacho C.', telefono: '965771111'}, 
    (error, resultado, campos) => {
        ...
});

Si hacemos un borrado o actualización, podemos obtener el número de filas afectadas, de esta forma:

conexion.query("DELETE FROM contactos WHERE id > 10", 
    (error, resultado, campos) => {
        if (error)
            console.log("Error al realizar el borrado");
        else
            console.log(resultado.affectedRows, 
                "filas afectadas");
});

Ejercicio 1:

Crea una carpeta llamada “LibrosMySQL” dentro de la carpeta “ProyectosNode/Ejercicios”. Crea una base de datos llamada libros en MySQL e importa el backup que tienes disponible comprimido aquí.

En el proyecto, inicializa el archivo package.json con npm init, e instala el módulo mysql2. Crea un archivo index.js que cargue este módulo (con require), y realice estas operaciones sobre la base de datos de libros:

5. Uso de mysql2con Promises (async/await)

La librería mysql2 dispone de una versión alternativa basada en Promises, que permite trabajar con async/await en lugar de callbacks. Esta forma de trabajo resulta más clara y legible, especialmente cuando se encadenan varias operaciones contra la base de datos.

Para utilizar esta versión es necesario importar el módulo mysql2/promise:

const mysql = require("mysql2/promise");

El resto de parámetros de conexión son los mismos que en los ejemplos anteriores. Habitualmente se combina esta versión con un pool de conexiones:

const pool = mysql.createPool({
    host: "localhost",
    user: "root",
    password: "root",
    database: "contactos",
    charset: "utf8mb4"
});

Consultas con async/await

Al trabajar con Promises, el método query devuelve una promesa que puede resolverse usando await. El resultado se obtiene en forma de array, donde el primer elemento contiene las filas devueltas por la consulta.

async function listarContactos() {
    try {
        const [resultado] = await pool.query(
            "SELECT * FROM contactos"
        );

        resultado.forEach((contacto) => {
            console.log(contacto.nombre, ":", contacto.telefono);
        });
    } catch (error) {
        console.log("Error al realizar la consulta:", error);
    }
}

listarContactos();

Consultas con parámetros

Las consultas parametrizadas funcionan del mismo modo que en la versión con callbacks, utilizando placeholders ?:

async function obtenerContacto(id) {
    const [resultado] = await pool.query(
        "SELECT * FROM contactos WHERE id = ?",
        [id]
    );
    return resultado;
}

Este mecanismo sigue siendo fundamental para evitar problemas de inyección SQL.

Operaciones de modificación

Las operaciones de inserción, actualización y borrado también devuelven información sobre el resultado de la operación:

async function borrarContacto(id) {
    const [resultado] = await pool.query(
        "DELETE FROM contactos WHERE id = ?",
        [id]
    );
    console.log("Filas afectadas:", resultado.affectedRows);
}

Cierre del pool

En aplicaciones servidoras, el pool suele mantenerse abierto durante toda la vida de la aplicación. Solo es necesario cerrarlo cuando la aplicación finaliza:

await pool.end();

El uso de mysql2/promise junto con async/await es una forma moderna y habitual de acceder a MySQL desde Node.js. Aunque en estos ejemplos se escriben consultas SQL manualmente, presenta un estilo de trabajo muy similar al que utilizan las librerías ORM, como Sequelize, que abstraen el acceso a la base de datos y simplifican la gestión de modelos y relaciones, tema que se abordará en el siguiente tema.