15 - Comunicación con el gestor de base de datos MySQL empleando el módulo 'mysql2'


Una actividad muy común de una aplicación web es el acceso a un gestor de base de datos. Veremos en este concepto como podemos comunicarnos desde nuestra aplicación con el servidor de base de datos MySQL.

Lo primero que debemos tener en cuenta es que tenemos que instalar el gestor de base de datos MySQL. Instalaremos el XAMPP y los pasos para dicha actividad los puede leer aquí.

El segundo paso será crear la base de datos que la llamaremos 'base1' y los pasos puede leerlos aquí. No crear tablas ya que nuestra aplicación se encargará de eso.

Problema

Desarrollar una aplicación web que permita administrar una tabla llamada 'articulos' en la base de datos 'base1'. Debemos poder crear la tabla, cargar registros, consultarlos y listarlos.

Como primer paso crearemos una carpeta donde localizaremos nuestro proyecto. Crear una carpeta llamada: ejercicio17. Dentro de esta carpeta crear un archivo 'ejercicio17.js' donde codificaremos la aplicación. Por otro lado crear una subcarpeta llamada 'public' y en dicha carpeta guardaremos todas las páginas estáticas de nuestra aplicación.

Como dijimos Node.js deja liberada a la comunidad para implementar módulos para comunicarse con otras aplicaciones. Para comunicarnos con MySQL existe un módulo llamado 'mysql2' que es un módulo más actualizado de otro llamado 'mysql'.

Desde la consola de comando nos posicionamos en la carpeta 'ejercicio17' y procedemos a instalar el módulo 'mysql2' mediante 'npm':

modulo mysql de node.js

Ahora tenemos creada una subcarpeta 'node_modules' con una subcarpeta 'mysql2' con el módulo propiamente dicho que nos facilitará la comunicación con MySQL.

En la carpeta public localizar los tres archivos HTML estáticos:

modulo mysql de node.js

index.html
<!doctype html>
<html>
<head>
  <title>Prueba</title>
</head>
<body>
   <a href="creartabla">Creacion de una tabla 'articulos' con MySQL</a></p>
   <a href="alta.html">alta de articulos</a></p>
   <a href="listado">Listado completo de articulos</a></p>   
   <a href="consulta.html">Consulta de un articulo por codigo</a></p>      
</body>
</html>
alta.html
<!doctype html>
<html>
<head>
</head>
<body>
  <form method="post" action="alta">
  Ingrese descripcion del articulo:
  <input type="descripcion" name="descripcion" size="50">
  <br>
  Ingrese el precio del articulo:
  <input type="text" name="precio" size="10">
  <br>
  <input type="submit" value="Agregar">
  </form>
</body>
</html>  
consulta.html
  
<!doctype html>
<html>
<head>
</head>
<body>
  <form method="post" action="consultaporcodigo">
  Ingrese el codigo del articulo a consultar:
  <input type="text" name="codigo" size="7">
  <br>
  <input type="submit" value="Consultar">
  </form>
</body>
</html>  

En la carpeta ejercicio17 localizamos el programa propiamente dicho:

modulo mysql de node.js

El código fuente en JavaScript para crear la tabla, efectuar el alta, listado y consultas es:

ejercicio17.js
const http = require('node:http')
const fs = require('node:fs')

const mysql = require('mysql2')

const conexion = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '',
  database: 'base1'
})

conexion.connect(error => {
  if (error)
    console.log('Problemas de conexion con mysql')
})


const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})

servidor.listen(8888)


function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/creartabla': {
      crear(respuesta)
      break
    }
    case 'public/alta': {
      alta(pedido, respuesta)
      break
    }
    case 'public/listado': {
      listado(respuesta)
      break
    }
    case 'public/consultaporcodigo': {
      consulta(pedido, respuesta)
      break
    }
    default: {
      fs.stat(camino, error => {
        if (!error) {
          fs.readFile(camino, (error, contenido) => {
            if (error) {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            } else {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            }
          })
        } else {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.write('<!doctype html><html><head></head><body>Recurso inexistente</body></html>')
          respuesta.end()
        }
      })
    }
  }
}


function crear(respuesta) {
  conexion.query('drop table if exists articulos', (error, resultado) => {
    if (error)
      console.log(error)
  })
  conexion.query(`create table articulos (
                                           codigo int primary key auto_increment,
                                           descripcion varchar(50),
                                           precio float
                                         )`, (error, resultado) => {
    if (error) {
      console.log(error)
      return
    }
  })
  respuesta.writeHead(200, { 'Content-Type': 'text/html' })
  respuesta.write(`<!doctype html><html><head></head><body>
                  Se creo la tabla<br><a href="index.html">Retornar</a></body></html>`)
  respuesta.end()
}


function alta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales;
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    const registro = {
      descripcion: formulario.get('descripcion'),
      precio: formulario.get('precio')
    }
    conexion.query('insert into articulos set ?', registro, (error, resultado) => {
      if (error) {
        console.log(error)
        return
      }
    })
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write(`<!doctype html><html><head></head><body>
                    Se cargo el articulo<br><a href="index.html">Retornar</a></body></html>`)
    respuesta.end()
  })
}


function listado(respuesta) {
  conexion.query('select codigo,descripcion,precio from articulos', (error, filas) => {
    if (error) {
      console.log('error en el listado')
      return
    }
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    let datos = ''
    for (let f = 0; f < filas.length; f++) {
      datos += 'Codigo:' + filas[f].codigo + '<br>'
      datos += 'Descripcion:' + filas[f].descripcion + '<br>'
      datos += 'Precio:' + filas[f].precio + '<hr>'
    }
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('<a href="index.html">Retornar</a>')
    respuesta.write('</body></html>')
    respuesta.end()
  })
}


function consulta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    const dato = [formulario.get('codigo')]
    conexion.query('select descripcion,precio from articulos where codigo=?', dato, (error, filas) => {
      if (error) {
        console.log('error en la consulta')
        return
      }
      respuesta.writeHead(200, { 'Content-Type': 'text/html' })
      let datos = ''
      if (filas.length > 0) {
        datos += 'Descripcion:' + filas[0].descripcion + '<br>'
        datos += 'Precio:' + filas[0].precio + '<hr>'
      } else {
        datos = 'No existe un artículo con dicho codigo.'
      }
      respuesta.write('<!doctype html><html><head></head><body>')
      respuesta.write(datos)
      respuesta.write('<a href="index.html">Retornar</a>')
      respuesta.write('</body></html>')
      respuesta.end()
    })
  })
}

console.log('Servidor web iniciado')

Iremos viendo por partes las distintas funcionalidades que tiene el programa para manipular una base de datos de MySQL.

Lo primero que hacemos es requerir el módulo 'mysql2' mediante la función require:

const mysql = require('mysql2')

Mediante la constante mysql llamamos a la función createConnection y le pasamos un objeto literal inicializando las propiedades 'host','user','password' y 'database'. Como dijimos ya creamos desde el PhpMyAdmin la base de datos 'base1':

const conexion = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '',
  database: 'base1'
})

Con la referencia a la conexión procedemos a llamar a connect para abrir la conexión con el servidor de base de datos (si hay algún error la función anónima traerá una referencia de dicho error):

conexion.connect(error => {
  if (error)
    console.log('Problemas de conexion con mysql')
})

Arranquemos nuestro programa desde la consola y desde el menú de opciones en el navegador elijamos la primer opción:

c:\ejerciciosnodejs\ejercicio17\node ejercicio17

modulo mysql de node.js

Tenemos en el hipervínculo que la propiedad href tiene el valor 'creartabla':

   <a href="creartabla">Creacion de una tabla 'articulos' con MySQL</a></p>

Desde Node.js capturamos la ruta indicada en el hipervínculo:

    case 'public/creartabla': {
      crear(respuesta)
      break
    }

En la función crear llamamos a la función query del objeto conexion que creamos previamente:

function crear(respuesta) {
  conexion.query('drop table if exists articulos', (error, resultado) => {
    if (error)
      console.log(error)
  })
  conexion.query(`create table articulos (
                                           codigo int primary key auto_increment,
                                           descripcion varchar(50),
                                           precio float
                                         )`, (error, resultado) => {
    if (error) {
      console.log(error)
      return
    }
  })
  respuesta.writeHead(200, { 'Content-Type': 'text/html' })
  respuesta.write(`<!doctype html><html><head></head><body>
                  Se creo la tabla<br><a href="index.html">Retornar</a></body></html>`)
  respuesta.end()
}

En la primer llamada de la función query le pasamos el comando SQL 'drop table if exists articulos' para que si ya existía la tabla proceda a borrarla.

En la segunda llamada a query le pasamos el comando SQL 'create table articulos ...' para que se cree la tabla.

Siempre que llamamos a query debemos pasarle además del string con el comando SQL un segundo parámetro que se trata de una función anónima que nos retorna un eventual error y los resultados que genera el comando SQL ejecutado.

Una vez que ejecutamos la primer opción ya tenemos creada la tabla. Pasemos a ver como funciona el alta en la tabla articulos. Cuando seleccionamos la segunda opción de la página index.html se solicita la página estática alta.html:

   <a href="alta.html">alta de articulos</a></p>

El servidor implementado con JavaScript (plataforma Node.js) devuelve dicha página al navegador que la solicita, donde el operador procede a cargar datos:

modulo mysql de node.js

Cuando se presiona el botón 'Agregar' vemos que la propiedad action del formulario tiene el valor 'alta':

  <form method="post" action="alta">

Este valor lo capturamos desde nuestro programa:

    case 'public/alta': {
      alta(pedido, respuesta)
      break
    }

La función alta:

function alta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales;
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    const registro = {
      descripcion: formulario.get('descripcion'),
      precio: formulario.get('precio')
    }
    conexion.query('insert into articulos set ?', registro, (error, resultado) => {
      if (error) {
        console.log(error)
        return
      }
    })
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write(`<!doctype html><html><head></head><body>
                    Se cargo el articulo<br><a href="index.html">Retornar</a></body></html>`)
    respuesta.end()
  })
}

Procede a rescatar todos los datos del formulario y a llamar a la función query de la variable conexion pasando el string con el comando SQL. El segundo parámetro de la función es un objeto literal donde inicializamos todos los campos de la tabla (menos el código de artículo ya que se genera automáticamente).

Si bien en SQL no existe la palabra clave set tengamos en cuenta que la función query procederá a generar un comando insert válido.

Nuevamente vemos que el tercer parámetro es la función anónima que se dispara luego que se ejecutó el comando SQL.

Finalmente respondemos al navegador con una página que generamos en forma dinámica.

El listado completo lo llamamos desde la página index.html:

   <a href="listado">Listado completo de articulos</a></p> 

Desde JavaScript capturamos la url que llega con el valor 'listado':

    case 'public/listado': {
      listado(respuesta)
      break
    }

En la función listado procedemos a mostrar todos los datos de la tabla 'articulos':

function listado(respuesta) {
  conexion.query('select codigo,descripcion,precio from articulos', (error, filas) => {
    if (error) {
      console.log('error en el listado')
      return
    }
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    let datos = ''
    for (let f = 0; f < filas.length; f++) {
      datos += 'Codigo:' + filas[f].codigo + '<br>'
      datos += 'Descripcion:' + filas[f].descripcion + '<br>'
      datos += 'Precio:' + filas[f].precio + '<hr>'
    }
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('<a href="index.html">Retornar</a>')
    respuesta.write('</body></html>')
    respuesta.end()
  })
}

En este algoritmos es muy importante lo que hacemos en la función anónima donde procedemos a mostrar todos los datos en la página HTML que generamos en forma dinámica:

modulo mysql de node.js

Lo único que nos queda es la consulta por el código. Desde la página index.html procedemos a solicitar la página estática consulta.html:

   <a href="consulta.html">Consulta de un articulo por codigo</a></p>      

Esta página estática solicita que se ingrese el código de artículo y procede a enviarlo al servidor indicando en la propiedad action del elemento 'form' el valor 'consultaporcodigo':

  <form method="post" action="consultaporcodigo">

En Node.js capturamos este valor:

    case 'public/consultaporcodigo': {
      consulta(pedido, respuesta)
      break
    }

Y llamamos a la función consulta donde rescatamos los valores del formulario y procedemos a llamar al comando SQL select con la clausula where indicando el código que cargó el operador en el formulario HTML:

function consulta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    const dato = [formulario.get('codigo')]
    conexion.query('select descripcion,precio from articulos where codigo=?', dato, (error, filas) => {
      if (error) {
        console.log('error en la consulta')
        return
      }
      respuesta.writeHead(200, { 'Content-Type': 'text/html' })
      let datos = ''
      if (filas.length > 0) {
        datos += 'Descripcion:' + filas[0].descripcion + '<br>'
        datos += 'Precio:' + filas[0].precio + '<hr>'
      } else {
        datos = 'No existe un artículo con dicho codigo.'
      }
      respuesta.write('<!doctype html><html><head></head><body>')
      respuesta.write(datos)
      respuesta.write('<a href="index.html">Retornar</a>')
      respuesta.write('</body></html>')
      respuesta.end()
    })
  })
}

Este proyecto lo puede descargar en un zip con todos los archivos desde este enlace : ejercicio17

Es importante conocer y visitar el sitio oficial de cada módulo que implementan los desarrolladores donde podemos enterarnos de las mejoras que le añaden. El sitio oficial de la extensión 'mysql2' lo puede visitar aquí

Implementación alternativa de mysql2 empleando promesas.

En la actualidad en JavaScript el tratamiento de procesos asincrónicos se los resuelve utilizando promesas, lo cual nos evita en gran medida los callback.

Podemos ver el código alternativo empleando promesas tanto para el acceso a la base de datos como el tratamiento de los archivos:

ejercicio17b.js
const http = require('node:http')
const fs = require('node:fs/promises')
const mysql = require('mysql2/promise')

async function conectarMysql() {
  const conexion = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'base1'
  })
  await conexion.connect()
  return conexion
}

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})

servidor.listen(8888)


async function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/creartabla': {
      crear(respuesta)
      break
    }
    case 'public/alta': {
      alta(pedido, respuesta)
      break
    }
    case 'public/listado': {
      listado(respuesta)
      break
    }
    case 'public/consultaporcodigo': {
      consulta(pedido, respuesta)
      break
    }
    default: {
      try {
        contenido = await fs.readFile(camino)
        const vec = camino.split('.')
        const extension = vec[vec.length - 1]
        const mimearchivo = mime[extension]
        respuesta.writeHead(200, { 'Content-Type': mimearchivo })
        respuesta.write(contenido)
        respuesta.end()
      } catch (error) {
        paginaError(respuesta, "Pagina inexistente")
      }
    }
  }
}

async function crear(respuesta) {
  try {
    const conexion = await conectarMysql()
    await conexion.query('drop table if exists articulos')
    await conexion.query(`create table articulos (
                                           codigo int primary key auto_increment,
                                           descripcion varchar(50),
                                           precio float
                                         )`)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write(`<!doctype html><html><head></head><body>
                  Se creo la tabla<br><a href="index.html">Retornar</a></body></html>`)
    respuesta.end()
    conexion.end()
  } catch (error) {
    paginaError(respuesta, "Problemas en la creacion de la tabla")
  }
}

async function alta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales;
  })
  pedido.on('end', async () => {
    const formulario = new URLSearchParams(info)
    const registro = {
      descripcion: formulario.get('descripcion'),
      precio: formulario.get('precio')
    }
    const conexion = await conectarMysql()
    await conexion.query('insert into articulos set ?', registro)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write(`<!doctype html><html><head></head><body>
                    Se cargo el articulo<br><a href="index.html">Retornar</a></body></html>`)
    respuesta.end()
    conexion.end()
  })
}

async function listado(respuesta) {
  const conexion = await conectarMysql()
  const [filas] = await conexion.query('select codigo,descripcion,precio from articulos')
  respuesta.writeHead(200, { 'Content-Type': 'text/html' })
  let datos = ''
  for (let f = 0; f < filas.length; f++) {
    datos += 'Codigo:' + filas[f].codigo + '<br>'
    datos += 'Descripcion:' + filas[f].descripcion + '<br>'
    datos += 'Precio:' + filas[f].precio + '<hr>'
  }
  respuesta.write('<!doctype html><html><head></head><body>')
  respuesta.write(datos)
  respuesta.write('<a href="index.html">Retornar</a>')
  respuesta.write('</body></html>')
  respuesta.end()
  conexion.end()
}

function consulta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', async () => {
    const formulario = new URLSearchParams(info)
    const dato = [formulario.get('codigo')]
    const conexion = await conectarMysql()
    const [filas] = await conexion.query('select descripcion,precio from articulos where codigo=?', dato)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    let datos = ''
    if (filas.length > 0) {
      datos += 'Descripcion:' + filas[0].descripcion + '<br>'
      datos += 'Precio:' + filas[0].precio + '<hr>'
    } else {
      datos = 'No existe un artículo con dicho codigo.'
    }
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('<a href="index.html">Retornar</a>')
    respuesta.write('</body></html>')
    respuesta.end()
    conexion.end()
  })
}

function paginaError(respuesta, error) {
  respuesta.writeHead(200, { 'Content-Type': 'text/html' })
  respuesta.write(`<!doctype html><html><head></head><body>
                    ${error}</body></html>`)
  respuesta.end()
}

console.log('Servidor web iniciado')

Por un lado requerimos el módulo para administrar archivos con promesas y lo mismo para trabajar con promesas con el módulo que descargamos:

const fs = require('node:fs/promises')
const mysql = require('mysql2/promise')

Implementamos una función asíncrona para poder hacer uso de await, las funciones createConnection y connect retornan promesas:

async function conectarMysql() {
  const conexion = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'base1'
  })
  await conexion.connect()
  return conexion
}

La función 'encaminar' ahora es asíncrona debido a que llamamos a la función 'readFile' que retorna una promesa:

async function encaminar(pedido, respuesta, camino) {
  switch (camino) {
    case 'public/creartabla': {
      crear(respuesta)
      break
    }
    case 'public/alta': {
      alta(pedido, respuesta)
      break
    }
    case 'public/listado': {
      listado(respuesta)
      break
    }
    case 'public/consultaporcodigo': {
      consulta(pedido, respuesta)
      break
    }
    default: {
      try {
        contenido = await fs.readFile(camino)
        const vec = camino.split('.')
        const extension = vec[vec.length - 1]
        const mimearchivo = mime[extension]
        respuesta.writeHead(200, { 'Content-Type': mimearchivo })
        respuesta.write(contenido)
        respuesta.end()
      } catch (error) {
        paginaError(respuesta, "Pagina inexistente")
      }
    }
  }
}

La función 'crear' también es asíncrona, porque tenemos que llamar a nuestra función de conectarMysql y también a query (como vemos nuestro código queda mucho más legible en lugar de utilizar funciones callback):

async function crear(respuesta) {
  try {
    const conexion = await conectarMysql()
    await conexion.query('drop table if exists articulos')
    await conexion.query(`create table articulos (
                                           codigo int primary key auto_increment,
                                           descripcion varchar(50),
                                           precio float
                                         )`)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    respuesta.write(`<!doctype html><html><head></head><body>
                  Se creo la tabla<br><a href="index.html">Retornar</a></body></html>`)
    respuesta.end()
    conexion.end()
  } catch (error) {
    paginaError(respuesta, "Problemas en la creacion de la tabla")
  }
}

La misma lógica de definir funciones asíncronas sucede con 'alta' y 'listado':

Por último la función 'consulta' no es asíncrona, salvo la función anónima que captura el evento 'end' donde accedemos nuevamente a la base de datos:

function consulta(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', async () => {
    const formulario = new URLSearchParams(info)
    const dato = [formulario.get('codigo')]
    const conexion = await conectarMysql()
    const [filas] = await conexion.query('select descripcion,precio from articulos where codigo=?', dato)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    let datos = ''
    if (filas.length > 0) {
      datos += 'Descripcion:' + filas[0].descripcion + '<br>'
      datos += 'Precio:' + filas[0].precio + '<hr>'
    } else {
      datos = 'No existe un artículo con dicho codigo.'
    }
    respuesta.write('<!doctype html><html><head></head><body>')
    respuesta.write(datos)
    respuesta.write('<a href="index.html">Retornar</a>')
    respuesta.write('</body></html>')
    respuesta.end()
    conexion.end()
  })
}

Retornar