Integrando H2 con Node.js y Express

En este tutorial, configuraremos una base de datos H2 en Node.js y Express y codificaremos la funcionalidad CRUD para una API REST.

Introducción

H2 es un servidor de base de datos liviano escrito en Java. Puede integrarse en aplicaciones Java o ejecutarse como un servidor independiente.

En este tutorial, revisaremos por qué H2 puede ser una buena opción para sus proyectos. También aprenderemos cómo integrar H2 con Node.js creando una API Express simple.

Las características de H2

H2 se creó pensando en el rendimiento.

"H2 es una combinación de: rápido, estable, fácil de usar y funciones".

Aunque H2 destaca principalmente porque puede integrarse en aplicaciones Java, tiene algunas características interesantes que también se aplican a su versión de servidor. Veamos algunos de ellos a continuación.

Tamaño y rendimiento

El archivo .jar utilizado para la versión del servidor es de alrededor de 2 MB. Podemos descargarlo del sitio H2, incluido con scripts y documentación adicionales. Sin embargo, si buscamos en Maven Central, podemos descargar el archivo .jar por sí solo.

El rendimiento de H2 brilla en su versión integrada. Aun así, el punto de referencia oficial demuestra que su versión cliente-servidor también es impresionante.

Bases de datos en memoria y cifrado

Las bases de datos en memoria no son persistentes. Todos los datos se almacenan en la memoria, por lo que la velocidad aumenta considerablemente.

El sitio H2 explica que las bases de datos en memoria son particularmente útiles cuando se crean prototipos o cuando se usan bases de datos de solo lectura.

El cifrado es otra característica útil para proteger los datos en reposo. Las bases de datos se pueden cifrar con el algoritmo AES-128.

Otras funciones útiles

H2 también proporciona un modo de clúster, la capacidad de ejecutar varios servidores y conectarlos entre sí. Las escrituras se realizan en todos los servidores al mismo tiempo, mientras que las lecturas se realizan desde el primer servidor del clúster.

H2 sorprende por su sencillez. Proporciona varias funciones útiles y es fácil de configurar.

Comencemos un servidor H2 en preparación para las siguientes secciones:

1
$ java -cp ./h2-1.4.200.jar org.h2.tools.Server -tcp -tcpAllowOthers -tcpPort 5234 -baseDir ./ -ifNotExists

Los argumentos que comienzan con tcp permiten la comunicación con el servidor. El argumento ifNotExists permite crear la base de datos al acceder a ella por primera vez.

Descripción de la API y Diagrama General

Supongamos que estamos escribiendo una API para registrar todos los exoplanetas encontrados hasta la fecha. Los exoplanetas son planetas que se encuentran fuera de nuestro Sistema Solar, orbitando otras estrellas.

If you're not yet familiar with the creation of REST APIs, read our Creación de una API REST con Node y Express!

Esta es nuestra simple Definición de API, un CRUD para uno recurso:

Definición de API REST

Esta definición junto con el resto del código que veremos a continuación está disponible en este repositorio de GitHub.

Así es como se verá nuestra aplicación al final de este tutorial:

General diagram

A la izquierda del diagrama vemos el Cliente API. Ese cliente puede ser la función "Pruébalo" del [Editor de arrogancia](https://editor.swagger.io/?url=https://raw.githubusercontent.com/lcofre/rest-api/ master/exoplanets.yml), o cualquier otro cliente, como Postman o cURL.

En el otro extremo encontramos el servidor de base de datos H2, ejecutándose en el puerto TCP 5234 como se explicó anteriormente.

Finalmente, nuestra aplicación en el medio se compone de dos archivos. El primero tendrá la aplicación Express que responderá a todas las solicitudes de API REST. Todos los puntos finales que describimos en la definición anterior se agregarán a este archivo.

El segundo archivo tendrá las funciones de persistencia para acceder a la base de datos para ejecutar las operaciones CRUD, usando el paquete JDBC.

Esquema de base de datos

Para almacenar el recurso del exoplaneta en una base de datos H2, primero debemos escribir las funciones CRUD básicas. Comencemos con la creación de la base de datos.

Usamos el paquete JDBC para acceder a bases de datos a través de JDBC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var JDBC = require('jdbc');
var jinst = require('jdbc/lib/jinst');

if (!jinst.isJvmCreated()) {
  jinst.addOption("-Xrs");
  jinst.setupClasspath(['../h2-1.4.200.jar']);
}

var h2 = new JDBC({
  url: 'jdbc:h2:tcp://localhost:5234/exoplanets;database_to_lower=true',
  drivername: 'org.h2.Driver',
  properties: {
    user : 'SA',
    password: ''
  }
});

var h2Init = false;

function getH2(callback) {
  if (!h2Init)
    h2.initialize((err) => {
      h2Init = true;
      callback(err)
    });
  return callback(null);
};

function queryDB(sql, callback) {
  h2.reserve((err, connobj) => {
    connobj.conn.createStatement((err, statement) => {
      if(callback) {
        statement.executeQuery(sql, (err, result) => h2.release(connobj, (err) => callback(result)));
      } else {
        statement.executeUpdate(sql, (err) => h2.release(connobj, (err) => { if(err) console.log(err) }));
      }
    });
  });
};

module.exports = {
  initialize: function(callback) {
    getH2((err) => {
      queryDB("CREATE TABLE IF NOT EXISTS exoplanets ("
        + "  id INT PRIMARY KEY AUTO_INCREMENT,"
        + "  name VARCHAR NOT NULL,"
        + "  year_discovered SIGNED,"
        + "  light_years FLOAT,"
        + "  mass FLOAT,"
        + "  link VARCHAR)"
      );
    });
  },

La función initialize() es bastante simple debido a las funciones auxiliares escritas de antemano. Crea la tabla de exoplanetas si aún no existe. Esta función debe ejecutarse antes de que nuestra API comience a recibir solicitudes. Más adelante veremos dónde hacerlo con Express.

El objeto h2 se configura con la cadena de conexión y las credenciales para acceder al servidor de la base de datos. Es más simple para este ejemplo, pero hay margen de mejora con respecto a la seguridad. Podríamos guardar nuestras credenciales en otro lugar, como variables de entorno, por ejemplo.

Además, necesitábamos agregar la ruta al archivo jar H2 en el método jinst.setupClasspath(). Esto se debe a que el paquete JDBC necesita un controlador para conectarse a H2, org.h2.Driver.

La cadena de conexión JDBC termina en /exoplanets;database_to_lower=true. Esto significa que al conectarse por primera vez se creará una base de datos llamada exoplanetas. Además, los nombres de las tablas y columnas se guardarán en minúsculas. Esto simplificará la API, por lo que no será necesario convertir los nombres de las propiedades.

La función queryDB() utiliza los métodos de la biblioteca JDBC para acceder a la base de datos. Primero, necesita reservar() una conexión a la base de datos. Los siguientes pasos son createStatement() y luego executeQuery() si se espera un resultado, o executeUpdate() en caso contrario. La conexión siempre se libera.

Todas las funciones anteriores pueden devolver un error. Para simplificar este ejemplo, todos los errores se dejan sin verificar, pero en un proyecto real deberíamos verificarlos.

La función getH2() devuelve un objeto que representa la base de datos. Creará ese objeto solo una vez, usando el mismo mecanismo que usan las clases Singleton para devolver solo una instancia siempre.

Ahora validemos los datos del usuario y permitámosles realizar operaciones CRUD.

Funciones de base de datos CRUD

Hagamos las funciones requeridas para permitir que esta aplicación realice operaciones CRUD en exoplanetas. Los agregaremos a module.exports para que podamos hacer referencia a ellos fácilmente desde otros archivos y crear un módulo auxiliar persistence.js que podamos usar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
module.exports = {
  getAll: function(callback) {
    getH2((err) => queryDB("SELECT * FROM exoplanets", (result) => {
      result.toObjArray((err, results) => callback(results))
    }));
  },
  get: function(id, callback) {
    getH2((err) => queryDB(`SELECT * FROM exoplanets WHERE id = ${id}`, (result) => {
      result.toObjArray((err, results) => { 
        return (results.length > 0) ? callback(results[0]) : callback(null);
      })
    }));
  },
  create: function(exoplanet) {
    getH2((err) => {
      columns = Object.keys(exoplanet).join();
      Object.keys(exoplanet).forEach((key) => exoplanet[key] = `'${exoplanet[key]}'`);
      values = Object.values(exoplanet).join();

      queryDB(`INSERT INTO exoplanets (${columns}) VALUES(${values})`);
    });
  },
  update: function(id, exoplanet) {
    getH2((err) => {
      keyValues = []
      Object.keys(exoplanet).forEach((key) => keyValues.push(`${key} = '${exoplanet[key]}'`));

      queryDB(`UPDATE exoplanets SET ${keyValues.join()} WHERE id = ${id}`);
    });
  },
  delete: function(id) {
    getH2((err) => queryDB(`DELETE FROM exoplanets WHERE id = ${id}`));
  },
};

Las funciones get() y getAll() consultan la base de datos para devolver uno o más exoplanetas. La API los devolverá directamente al cliente de la API.

Todas las funciones son principalmente consultas SQL, pero create() y update() merecen más explicación.

La declaración SQL INSERT puede recibir columnas y valores separados, en la forma INSERT INTO table (column1Name) VALUES ('column1Value'). Podemos usar el método join() para generar una cadena de columnas separadas por comas y hacer algo similar para unir todos los valores que queremos en la función create().

La instrucción SQL UPDATE es un poco más compleja. Su forma es UPDATE table SET column1Name = 'column1Value'. Así que necesitamos crear una nueva matriz en la función update() para almacenar los valores en este formato y unirlos() más tarde.

Guardemos todas las funciones de la base de datos en su propio archivo, persistence.js, para que podamos agregar algo de contexto cuando llamemos a las funciones en el archivo API, así:

1
2
const persistence = require('./persistence');
persistence.getAll();

Jueves Esquema

Como regla general, siempre debemos validar lo que envía un usuario antes de usarlo, por ejemplo, cuando el usuario intenta crear un recurso.

Algunos paquetes facilitan esta tarea. Usaremos Joi para lograr la validación.

Primero, necesitamos definir un esquema de nuestro recurso, una definición de propiedades y sus tipos. Nos recuerda la instrucción SQL CREATE que definimos antes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const Joi = require('joi');

const exoplanetSchema = Joi.object({
    id: Joi.number(),
    name: Joi.string().required(),
    year_discovered: Joi.number(),
    light_years: Joi.number(),
    mass: Joi.number(),
    link: Joi.string().uri()
})
options({ stripUnknown: true });

Cada tipo impondrá alguna validación. Por ejemplo, la propiedad enlace debe parecerse a un URI, y el nombre es requerido().

Luego podemos validar un recurso usando el método exoplanetSchema.validate(theObject). Este método devolverá un objeto con una propiedad error con errores de validación si los hubiera, y una propiedad value con el objeto procesado. Usaremos esta validación al crear y actualizar un objeto.

Para agregar robustez a nuestra API, sería bueno ignorar y descartar cualquier propiedad adicional que no esté incluida en nuestro esquema. Esto se logra en la definición anterior configurando la opción stripUnknown en true.

API REST con Express

Usaremos el paquete Express para crear nuestra API REST. Y como acabamos de ver, también usaremos Joi para validar recursos.

Configuremos un servidor Express regular:

1
2
3
4
5
6
const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(express.json());

La variable de la aplicación es nuestra API, vacía por ahora. Express permite ampliar su funcionalidad mediante el uso de middleware, funciones que pueden modificar las solicitudes y respuestas de nuestra API. En este caso, estamos utilizando dos middlewares.

Primero, cors() permitirá que otras aplicaciones de navegador llamen a nuestra API. Esto incluye el Editor de arrogancia que podemos usar para probar nuestro API más tarde. Si desea leer más sobre Manejo de CORS con Node.js y Express, lo tenemos cubierto.

En segundo lugar, agregamos el middleware express.json() para habilitar el análisis de objetos JSON en el cuerpo de las solicitudes.

Agreguemos ahora algunos puntos finales a la API. Comenzaremos con post() y put(), ya que usan la validación Joi explicada en la última sección:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app.post('/exoplanets', (req, res) => {
    delete req.body.id;
    const { error, value } = exoplanetSchema.validate(req.body);
    if(error)
        res.status(405).send(error.details[0].message);

    persistence.create(value);
    res.status(201);
});

app.put('/exoplanets/:id', (req, res) => {
    delete req.body.id;
    const { error, value } = exoplanetSchema.validate(req.body);
    if(error) {
        res.status(405).send(error.details[0].message);
    }

    persistence.get(req.params.id, (result) => {
        if(result) {
            persistence.update(req.params.id, value);
            res.status(201);
        } else {
            res.status(404);
        }
    });
});

Express admite una función por verbo HTTP, por lo que en este caso tenemos post() y put() como dos funciones.

En ambas funciones, el recurso se valida primero y cualquier “error” se devuelve al cliente API. Para simplificar este código, en ese caso solo se devuelve el primer error de validación.

put() también verifica si el recurso existe al intentar obtenerlo de la base de datos. Actualizará el recurso solo si existe.

Con las funciones post() y put() que requieren validación fuera del camino, manejemos los métodos get() cuando los usuarios deseen echar un vistazo a los exoplanetas, así como a los Función delete() utilizada para eliminar un exoplaneta de la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
app.get('/exoplanets', (req, res) => persistence.getAll((result) => res.send(result)));

app.get('/exoplanets/:id', (req, res) => {
    persistence.get(req.params.id, (result) => {
        if(result)
            res.send(result);
        else
            res.status(404);
    });
});

app.delete('/exoplanets/:id', (req, res) => {
    persistence.get(req.params.id, (result) => {
        if(result) {
            persistence.delete(req.params.id);
            res; 
        } else {
            res.status(404);
        }            
    });
});

Habiendo definido todos los puntos finales, configuremos el puerto en el que la aplicación escuchará las solicitudes en:

1
2
3
4
app.listen(5000, () => {
    persistence.initialize();
    console.log("Exoplanets API listening at http://localhost:5000")
});

La devolución de llamada anterior se llamará solo una vez al iniciar el servidor, por lo que es el lugar perfecto para inicializar () la base de datos.

Conclusión

H2 es un servidor de base de datos útil, eficaz y fácil de usar. Aunque es un paquete de Java, también se ejecuta como un servidor independiente, por lo que podemos usarlo en Node.js con el paquete JDBC.

En este tutorial, definimos primero un CRUD simple para ilustrar cómo acceder a la base de datos y qué funciones están disponibles. Después de eso, definimos una API REST con Express. Esto nos ayudó a tener una idea más completa sobre cómo recibir recursos y guardarlos en H2.

Aunque se omitieron varios conceptos en aras de la brevedad, como autenticación y paginación, este tutorial es una buena referencia para comenzar a usar H2 en nuestros proyectos Express.