Integrando H2 con Python y Flask

En este tutorial, usaremos el paquete JayDeBeApi para conectar Python al controlador de la base de datos H2. Hemos creado una aplicación RESTful Flask con capacidades CRUD.

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 Python creando una API simple de Flask.

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.

Si aún no estás familiarizado con la creación de APIs REST, lee nuestra [Guía completa crear para API REST con Spring Boot](/construir-una-api-spring-boot-rest-con-la-guia- completa-de-java/)!

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 tres archivos de Python. El primero tendrá la aplicación Flask 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, que acceden a la base de datos para ejecutar las operaciones CRUD, utilizando el paquete JayDeBeApi.

Por último, un tercer archivo contendrá un esquema que representa el recurso que administra la API, el ‘Exoplaneta’. Usaremos el paquete Marshmallow para representar ese esquema. Los primeros dos archivos de python usarán este esquema para representar recursos y pasarlos entre sí.

Comencemos con el archivo de persistencia.

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 por escribir la creación de la base de datos. Usamos el paquete jaydebeapi 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
import jaydebeapi

def initialize():
    _execute(
        ("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)"))

def _execute(query, returnResult=None):
    connection  = jaydebeapi.connect(
        "org.h2.Driver",
        "jdbc:h2:tcp://localhost:5234/exoplanets",
        ["SA", ""],
        "../h2-1.4.200.jar")
    cursor = connection.cursor()
    cursor.execute(query)
    if returnResult:
        returnResult = _convert_to_schema(cursor)
    cursor.close()
    connection.close()

    return returnResult

La función initialize() es bastante simple debido a las funciones auxiliares posteriores. Crea la tabla de exoplanetas si aún no existe. Esta función debe ejecutarse antes de que nuestra API comience a recibir solicitudes. Veremos más adelante dónde hacer eso con Flask.

La función _execute() contiene 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, agregamos la ruta al archivo jar H2 al método connect(), ya que tiene el controlador que necesitamos para conectarnos a H2 - org.h2.Driver.

La cadena de conexión JDBC termina en /exoplanets. Esto significa que al conectarse por primera vez se creará una base de datos llamada exoplanetas.

Es posible que hayas notado que _execute() puede devolver el resultado de la consulta SQL usando la función _convert_to_schema(). Veamos ahora cómo funciona esa función.

Marshmallow Esquemas y funciones de base de datos CRUD

Algunas consultas SQL devuelven resultados tabulares, particularmente la instrucción SELECT. JayDeBeApi formateará esos resultados como una lista de tuplas. Por ejemplo, para el esquema definido en la última sección podríamos obtener un resultado similar a este:

1
2
3
4
>>> connection  = jaydebeapi.connect(...
>>> cursor = connection.cursor()
>>> cursor.execute("SELECT * FROM exoplanets")
>>> cursor.fetchall()
1
[(1, 'Sample1', 2019, 4.5, 1.2, 'http://sample1.com')]

Nada nos impide administrar los resultados en este formato y, finalmente, devolverlo al cliente API. Pero de cara al futuro, sabemos que usaremos Flask, por lo que sería bueno devolver los resultados en un formato recomendado por Flask.

En particular, usaremos Flask-RESTful para facilitar el uso de rutas API. Ese paquete recomienda usar Malvavisco para analizar solicitudes. Este paso permite la normalización de los objetos. De esta forma podemos descartar propiedades desconocidas y resaltar errores de validación, por ejemplo.

Veamos cómo se vería la clase Exoplanet para que podamos discutir más:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from marshmallow import Schema, fields, EXCLUDE

class ExoplanetSchema(Schema):
    id = fields.Integer(allow_none=True)
    name = fields.Str(required=True, error_messages={"required": "An exoplanet needs at least a name"})
    year_discovered = fields.Integer(allow_none=True)
    light_years = fields.Float(allow_none=True)
    mass = fields.Float(allow_none=True)
    link = fields.Url(allow_none=True)
    class Meta:
        unknown = EXCLUDE

La definición de las propiedades parece familiar. Es lo mismo que el esquema de la base de datos, incluida la definición de los campos obligatorios. Todos los campos tienen un tipo que define alguna validación por defecto. Por ejemplo, el campo enlace se define como una URL, por lo que una cadena que no parece una URL no será válida.

Aquí también se pueden incluir mensajes de error específicos, como la validación de un nombre.

Para este proyecto de ejemplo, queremos descartar o excluir todos los campos desconocidos y el cliente API puede enviar erróneamente. Esto se logra en la clase anidada Meta.

Ahora podemos usar los métodos load() y loads() Marshmallow para convertir y validar nuestros recursos.

Ahora que estamos familiarizados con Marshmallow, podemos explicar lo que hace _convert_to_schema():

1
2
3
4
5
def _convert_to_schema(cursor):
    column_names = [record[0].lower() for record in cursor.description]
    column_and_values = [dict(zip(column_names, record)) for record in cursor.fetchall()]

    return ExoplanetSchema().load(column_and_values, many=True)

En JayDeBeApi, los nombres de las columnas se guardan en el campo descripción del cursor, mientras que los datos se pueden obtener con el método fetchall(). Usamos listas de comprensión en las dos primeras líneas para obtener los nombres y valores de las columnas, y zip() para fusionarlos.

La última línea toma el resultado combinado y los convierte en objetos ExoplanetSchema que Flask puede seguir procesando.

Ahora que explicamos la función _execute() y la clase ExoplanetSchema, veamos todas las funciones de la base de datos CRUD:

 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
def get_all():
    return _execute("SELECT * FROM exoplanets", returnResult=True)

def get(Id):
    return _execute("SELECT * FROM exoplanets WHERE id = {}".format(Id), returnResult=True)

def create(exoplanet):
    count = _execute("SELECT count(*) AS count FROM exoplanets WHERE name LIKE '{}'".format(exoplanet.get("name")), returnResult=True)
    if count[0]["count"] > 0:
        return

    columns = ", ".join(exoplanet.keys())
    values = ", ".join("'{}'".format(value) for value in exoplanet.values())
    _execute("INSERT INTO exoplanets ({}) VALUES({})".format(columns, values))

    return {}

def update(exoplanet, Id):
    count = _execute("SELECT count(*) AS count FROM exoplanets WHERE id = {}".format(Id), returnResult=True)
    if count[0]["count"] == 0:
        return

    values = ["'{}'".format(value) for value in exoplanet.values()]
    update_values = ", ".join("{} = {}".format(key, value) for key, value in zip(exoplanet.keys(), values))
    _execute("UPDATE exoplanets SET {} WHERE id = {}".format(update_values, Id))

    return {}

def delete(Id):
    count = _execute("SELECT count(*) AS count FROM exoplanets WHERE id = {}".format(Id), returnResult=True)
    if count[0]["count"] == 0:
        return

    _execute("DELETE FROM exoplanets WHERE id = {}".format(Id))
    return {}

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 la función join() para fusionar todas las columnas y separarlas con comas, y hacer algo similar para unir todos los valores que queremos insertar.

La instrucción SQL UPDATE es un poco más compleja. Su forma es UPDATE table SET column1Name = 'column1Value'. Así que necesitamos alternar claves y valores, y lo hicimos usando la función zip().

Todas estas funciones devuelven Ninguno cuando hay un problema. Posteriormente cuando les llamemos tendremos que comprobar ese valor.

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

1
2
3
import persistence

persistence.get_all()

API REST con Frask

Ahora que escribimos una capa para abstraer el acceso a la base de datos, estamos listos para escribir la API REST. Usaremos los paquetes Flask y Flask-RESTful para que nuestra definición sea lo más fácil posible. Como aprendimos antes, también usaremos Marshmallow para validar recursos.

Flask-RESTful requiere definir una clase por recurso API, en nuestro caso, solo el recurso Exoplanet. Entonces podemos asociar ese recurso con una ruta como esta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

class Exoplanet(Resource):
    # ...

api.add_resource(Exoplanet, "/exoplanets", "/exoplanets/<int:Id>")

De esta forma todas nuestras rutas, /exoplanets y /exoplanets/<int:Id> estarán dirigidas a la clase que definimos.

Por ejemplo, el punto final GET /exoplanets será respondido por un método llamado get() dentro de la clase Exoplanet. Debido a que también tenemos el punto final GET /exoplanet/<Id>, ese método get() debe tener un parámetro opcional llamado Id.

Veamos a toda la clase para entender esto mejor:

 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
from flask import request
from flask_restful import Resource, abort
from marshmallow import ValidationError
import persistence

class Exoplanet(Resource):
    def get(self, Id=None):
        if Id is None:
            return persistence.get_all()

        exoplanet = persistence.get(Id)
        if not exoplanet:
            abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})
        return exoplanet

    def post(self):
        try:
            exoplanet = ExoplanetSchema(exclude=["id"]).loads(request.json)
            if not persistence.create(exoplanet):
                abort(404, errors={"errors": {"message": "Exoplanet with name {} already exists".format(request.json["name"])}})
        except ValidationError as e:
            abort(405, errors=e.messages)

    def put(self, Id):
        try:
            exoplanet = ExoplanetSchema(exclude=["id"]).loads(request.json)
            if not persistence.update(exoplanet, Id):
                abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})
        except ValidationError as e:
            abort(405, errors=e.messages)

    def delete(self, Id):
        if not persistence.delete(Id):
            abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})

Los verbos HTTP restantes se procesan de la misma manera que GET, por los métodos con el nombre post(), put() y delete() .

Como dijimos antes, los errores lógicos al acceder a la base de datos harán que las funciones devuelvan Ninguno. Esos errores se capturan aquí cuando es necesario.

Además, las excepciones que representan errores de validación son desencadenadas por Marshmallow, por lo que esos errores también se capturan y se devuelven al usuario junto con un error de retorno apropiado.

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 puede ejecutarse como un servidor independiente, por lo que podemos usarlo en Python con el paquete JayDeBeApi.

En este tutorial, definimos una aplicación 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 Flask y Flask-RESTful.

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 Flask.