Las clases y librerías básicas de Java para bases de datos relacionales

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Las aplicaciones suelen delegar el guardado del estado y de los datos que tratan en sistemas especializados en almacenar datos. Las bases de datos relacionales son sistemas que han probado su eficacia y utilizad durante las últimas décadas de la computación. Aún con la aparición de múltiples bases de datos NoSQL alternativas las bases de datos relacionales se seguirán utilizando o incluso combinando diferentes tipos de bases de datos en un mismo sistema. Java proporciona desde sus primeras versiones el paquete java.sql con varias clases para el acceso a bases de datos relacionales, aunque el acceso a las bases de datos relacionales se suele utilizar a través de librerías es útil conocer estas clases de Java que constituyen los conceptos fundamentales de acceso a base de datos y que las librerías internamente son las que usan.

Java

La mayoría de las aplicaciones usan una base de datos para guardar persistir, consultar datos o guardar el estado de la aplicación. Las bases de datos son los sistemas especializados en guardar la información de las aplicaciones.

Dentro de la categoría de bases de datos las relacionales son unas de las más utilizadas por sus propiedades ACID y el potente lenguaje de consultas SQL. Las bases de datos relacionales siguen siendo adecuadas para muchos usos y propósitos aún con la aparición de las bases de datos NoSQL también útiles cuando existe ciertos requerimientos.

Clases del JDK para acceso a base de datos relacionales

Java ofrece soporte para las bases de datos relacionales desde prácticamente las primeras versiones del JDK hasta día de hoy incorporando un conjunto de clases en el paquete java.sql en la denominada API en Java de Java Database Connectivity o JDBC.

Las principales clases de la API de JDBC son las clases Statement y PreparedStatement que representan la sentencia SQL ya sea de inserción, actualización, eliminación o consulta así como las sentencias DDL para la creación de tablas, índices, campos o procedimientos almacenados. Normalmente se utiliza la clase PreparedStatement ya que tiene beneficios en cuanto a rendimiento al ejecutarse de forma repetida y utilizada correctamente permite evitar el grave problema de seguridad de SQL injection común en las aplicaciones que construyen de forma dinámica con concatenaciones sentencias SQL utilizando datos procedentes de fuentes no confiables como parámetros de una petición HTTP, de JSON u otras fuentes externas a la aplicación.

La clase ResultSet es la clase que proporciona el acceso a los datos cuando se ejecuta una sentencia SQL de consulta, la clase se itera en los resultados y se obtienen los datos según los nombres o índices asignados en la consulta para las columnas.

La clase Connection representa una conexión a una base de datos, hay que crear una conexión ya que las bases de datos trabajan con una arquitectura de cliente y servidor, la aplicación actúa de cliente y la base de datos actúa de servidor. A través de la clase Connection se inicia una transacción y finaliza con el commit o el rollback. Para crear la conexión de la base de datos se proporcionan las credenciales de usuario y contraseña.

Las bases de datos relacionales soportan transacciones para proporcionar las propiedades ACID para lo que se utilizan las transacciones. Atomicidad donde un grupo de operaciones individuales se ejecutan todas o ninguna, consistencia mediante la cual los cambios son válidos según las reglas incluyendo restricciones, cambios en cascada, y disparadores, aislamiento donde los cambios de una transacción no se ven afectados por los cambios realizados en otras transacciones concurrentes y finalmente durabilidad que garantiza que en caso de completarse la transacción perdura en el tiempo aún cuando el sistema sufra un fallo posterior.

Crear una conexión a una base de datos es costoso, para evitar incurrir en este tiempo de creación y destrucción de conexiones o limitar el número de conexiones que una aplicación utiliza como máximo las aplicaciones utilizan un pool de conexiones. Las conexiones se crean al iniciar la aplicación o bajo demanda según se van necesitando más hasta el límite máximo definido. Cuando la aplicación necesita una conexión la obtiene de forma rápida del pool de conexiones y cuando termina de utilizarla la devuelve al pool de conexiones para que sea reutilizada en posteriores usos.

Cada base de datos utiliza un protocolo diferente de comunicación con los clientes por lo que es necesario un componente que abstrae de las peculiaridades de cada base de datos y proporcione un marco común de trabajo independiente de cada base de datos. Cada base de datos requiere de un Driver compatible también con la versión de la base de datos. Generalmente, son los desarrolladores de la propia base de datos los que proporcionan un driver específico adecuado para el acceso a la base de datos desde Java que cumple con las APIs de JDBC.

En el tutorial sobre SQL con Java se explican los conceptos básicos y fundamentales para usar bases de datos relacionales.

Ejemplo de conexión y consulta a un base de datos relacional con la API de Java

En este ejemplo de código se muestra el uso de las clases fundamentales de Java para usar una base de datos relacional. El primer paso es establecer una conexión con la base de datos, en este caso usando la base de datos H2 en memoria.

Posteriormente se ejecuta una sentencia DDL para crear una tabla, se insertan varias filas con la sentencia insert y se obtienen los datos de la tabla con una sentencia select. Después de las inserciones se realiza un commit que completa una transacción en la base de datos, si en vez del commit se hiciese un rollback al obtener los datos con la consulta posterior la tabla aparecería vacía.

Las clases Connection, Statemente, PreparedStatement y ResultSet al finalizar su uso hay que invocar su método close para liberar los recursos que tienen reservados, especialmente en el caso de las conexiones ya que son un recurso limitado. Estas clases implementan la interfaz AutoCloseable con lo que son adecuadas para las sentencias try-with-resources de Java.

Establecer la conexión a la base de datos

Por defecto después de cada sentencia Java emite un commit, esto no es lo deseado en el caso de querer agrupar la ejecución de varias sentencias en una transacción, para evitarlo hay que usar la opción setAutoCommit a false.

 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
package io.github.picodotdev.blgbitix.javasql;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.text.DecimalFormat;
import java.util.Locale;

public class Main {

    public static void main(String[] args) {
        DriverManager.drivers().forEach(d -> {
            System.out.printf("Driver: %s%n", d.getClass().getName());
        });

        try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:database", "sa", "")) {
            connection.setAutoCommit(false);

            ...
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Main-1.java

Ejecutar una sentencia con Statement

1
2
Statement statement = connection.createStatement();
statement.execute("CREATE TABLE product(id INT IDENTITY NOT NULL PRIMARY KEY, name VARCHAR(255), price DECIMAL(20, 2))");
Main-2.java

Ejecutar una sentencia con PreparedStatement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO product (name, price) values (?, ?)", new String[] { "id" });

preparedStatement.setString(1, "PlayStation 5");
preparedStatement.setBigDecimal(2, new BigDecimal("499.99"));
preparedStatement.executeUpdate();

ResultSet resultSet1 = preparedStatement.getGeneratedKeys();
while (resultSet1.next()) {
    System.out.printf("Primary key: %s%n", resultSet1.getLong(1));
}
resultSet1.close();

preparedStatement.setString(1, "Xbox Series X");
preparedStatement.setBigDecimal(2, new BigDecimal("499.99"));
preparedStatement.executeUpdate();

ResultSet resultSet2 = preparedStatement.getGeneratedKeys();
while (resultSet2.next()) {
    System.out.printf("Primary key: %s%n", resultSet2.getLong(1));
}
resultSet2.close();

connection.commit();
Main-3.java

Ejecutar una consulta

1
2
3
4
5
6
7
PreparedStatement preparedStatement = connection.prepareStatement("SELECT id, name, price FROM product");

ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
    System.out.printf("Product (id: %s, name: %s, price: %s)%n", resultSet.getLong(1), resultSet.getString(2), DecimalFormat.getCurrencyInstance(new Locale("es", "ES")).format(resultSet.getBigDecimal(3)));
}
resultSet.close();
Main-4.java

Cerrar la conexión de forma explícita

1
2
connection.close();

Main-5.java

Resultado

El resultado del programa en la terminal es el siguiente.

1
2
3
4
5
Driver: org.h2.Driver
Primary key: 1
Primary key: 2
Product (id: 1, name: PlayStation 5, price: 499,99 €)
Product (id: 2, name: Xbox Series X, price: 499,99 €)
System.out

Dependencia con el driver de la base de datos

En el archivo de construcción hay quu añadir la dependencia que contiene el driver para la base de datos a conectarse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.h2database:h2:1.4.200'
}

application {
    mainClass = 'io.github.picodotdev.blgbitix.javasql.Main'
}
build.gradle

El problema de seguridad de SQL injection

El problema de seguridad de SQL injection es un grave problema de seguridad que afecta a las aplicaciones que construyen sentencias de forma dinámica a partir de datos provenientes de origen no confiable. Un origen no confiable es cualquier dato proveniente de forma externa a la aplicación, en el caso de las aplicaciones web o servicios REST es un parámetro de la petición o un dato de un JSON.

El SQL injection es un grave problema de seguridad ya que permite al atacante tener acceso a datos de la base de datos, obtener acceso con una cuenta de otro usuario o realizar operaciones sobre los datos de forma no autorizada. El problema se produce en la construcción de forma dinámica de la sentencia SQL mediante la concatenación de cadenas y datos de origen no confiable.

Las siguientes sentencias SQL sufren del problema de SQL injection, en la primera un atacante puede obtener un dato de cualquier columna de la tabla y con la segunda ejecutar una sentencia en este caso para eliminar todas las filas de cualquier tabla.

En la primera cambiando el valor de column se obtiene el dato de cualquier columna de la tabla, por ejemplo el campo password.

1
2
3
"SELECT id, " + column + " FROM users WHERE user_id = " + userId

"SELECT id, password FROM users WHERE user_id = 1
sql-injection-1.sql

En esta sentencia se finaliza la sentencia original con ; y se inicia otra lo que provoca la eliminación de una tabla, con un valor para user_id especialmente construido para ejecutar la sentencia maliciosa de eliminación de la tabla.

1
2
3
"SELECT * FROM users WHERE user_id = " + userId

"SELECT * FROM users WHERE user_id = 105; DROP TABLE users;"
sql-injection-2.sql

La solución al problema de seguridad de SQL injection en Java es no construir la sentencia de forma dinámica mediante concatenación de cadenas utilizando la clase PreparedStatement con argumentos para los datos que se inserten en la sentencia SQL.

1
2
3
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM users WHERE user_id = ?"});
preparedStatement.setInt(1, 1);
preparedStatement.executeUpdate();
sql-injection-prepared-statement.java

Librerías de persistencia en Java

Habitualmente no se utilizan directamente las clases de la API de Java sino que se utilizan otras librerías de más alto nivel. Una de las más conocidas es Hibernate, es un ORM que proporciona acceso a los datos con una correspondencia entre el modelo relacional de las bases de datos y el modelo de objetos de Java. La aplicación trabaja con objetos y relaciones entre los objetos e Hibernate se encarga de transformar esos objetos en el modelo relacional de la base de datos, la aplicación no ejecuta sentencias SQL de forma directa sino que es Hibernate el encargado de emitir las sentencias adecuadas según los cambios realizados en los datos. Es la implementación más utilizada de ORM en Java para la especificación JPA.

Spring Data es una capa de abstracción para el acceso a datos ya sean de un modelo relacional, de bases de datos NoSQL o algunos otros sistemas de datos. Spring Data hace más sencillo el acceso a los datos utilizando de forma subyacente JDBC o JPA. Spring Data proporciona algunas clases e interfaces que la aplicación implementa.

jOOQ también es otra librería de acceso a bases de datos relacionales, proporciona DSL para la construcción de sentencias SQL mediante un API Java. A diferencia de JPA con su lenguaje JPQL, jOOQ soporta características avanzadas del lenguaje SQL como windows functions. Otra ventaja de jOOQ es que al utilizar su DSL el compilador de Java realiza validación de tipos y comprobaciones en la sintaxis de la construcción de la SQL.

Algunas aplicaciones combinan el uso de varias de estas librerías en la misma aplicación según el caso, por ejemplo utilizando Hibernate para el modelo de escritura y jOOQ o Spring Data para el modelo de lectura. También es posible utilizar jOOQ para generar las sentencias SQL y posteriormente ejecutarlas con Hibernate o Spring Data.

Liquibase es otra librería de utilidad que permite lanzar scripts de migración con sentencias SQL para realizar cambios en la base de datos como modificar el esquema de tablas, insertar, actualizar o eliminar datos. Es necesario si los cambios en el código en una nueva versión de la aplicación requiere cambios en el esquema de la base de datos.

Estas no son las únicas librerías existentes pero sí son de las más conocidas y utilizadas.

Hibernate Spring jOOQ

Bases de datos relacionales

Las bases de datos de software libre más utilizadas son PostgreSQL, MariaDB y MySQL que rivalizan con la base de datos Oracle comercial. PostgreSQL es adecuada incluso para organizaciones y proyectos de gran tamaño.

Otras bases de datos relevantes son H2 una base de datos implementada en Java con características avanzadas que es posible utilizar para los teses de integración al no requerir de un servidor y ser posible ejecutarla en memoria. Para realizar las pruebas de integración utilizando la misma base de datos que en producción es posible utilizar Testcontainers que utiliza Docker para iniciar una instancia de la base de datos en un contenedor.

La conexión a la base de datos por seguridad requiere de un usuario y contraseña que la aplicación ha de conocer, para aún mayor seguridad es posible generar las credenciales de conexión a la base de datos de forma dinámica por Vault en una aplicación de Spring.

PostgreSQL MariaDB MySQL

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: