Curso Jakarta EE 9 (18). JPA con Hibernate (1): Introducción.

logo Jakarta EE

En este nuevo y extenso bloque del curso empezamos a crear nuestra aplicación de ejemplo para gestionar gastos personales y presupuestos, y lo hacemos desde «abajo», esto es, la capa de persistencia. Los datos de la aplicación se guardarán en MySQL, la base de datos de código abierto más popular.

>>>> ÍNDICE <<<<

Las bases de datos relacionales son un pilar fundamental de la práctica totalidad de aplicaciones empresariales y de gestión. Esto convierte a la especificación Jakarta Persistence \ JPA (Java Persistence API), creada para facilitar su empleo, en una de las más importantes y utilizadas. Su difusión incluso trasciende el ámbito de Jakarta EE y los servidores de aplicaciones, y la encontraremos en programas Java de todo tipo, incluyendo las desarrolladas con Spring.

Como ya viene siendo habitual, primero veamos un poco de teoría en este capítulo introductorio.

Bases de datos relaciones

Pese a la creciente apuesta por el mundo no SQL, cajón de sastre para productos muy diversos, las bases de datos relacionales siguen dominando el mercado desde su advenimiento décadas atrás y el panorama no parece que vaya a cambiar.

En estas bases de datos, que siendo rigurosos debemos llamarlas sistemas de gestión de bases de datos relacionales (SGDB o, en inglés, RDBMS), la información se organiza siguiendo el denominado «modelo relacional» propuesto por Codd en 1970, y llevado comercialmente a la práctica por Oracle e IBM a finales de la década. No es mi intención examinar este modelo (la bibliografía en español es amplia), pero sí voy a indicar sin entrar en detalles sus fundamentos para poner en contexto al lector. Volveremos sobre ellos en futuros capítulos.

En el centro del modelo relacional se encuentra el intuitivo concepto de tabla o relación. La base de datos puede concebirse como una colección de ellas. Cada una posee una serie de columnas que representan ciertos datos simples (un número, un texto, una fecha…), atributos que en conjunto definen a un concepto, por ejemplo «usuario». Cada elemento individual de ese tipo (un usuario) constituye una fila de la tabla, y se denomina registro o tupla.

Esta captura de pantalla muestra una hipotética tabla usuarios con la forma visual, valga la redundancia, de una tabla.

tabla bd

Las columnas pueden configurarse de manera que sus valores sean únicos y no se repitan en toda la tabla. También es posible establecer si admiten valores vacíos. Es el caso de la columna email del segundo registro en la imagen anterior.

Existen dos columnas especiales.

  • Clave primaria (Primary Key, PK). Permite identificar unívocamente a cada registro. Solo habrá una clave primaria por tabla, siendo además un atributo único y no vacío. Es posible que sea múltiple (compuesta) y esté formada por más de una columna.
  • Clave ajena, foránea o externa (Foreign Key, FK). Su valor relaciona dos registros ubicados en la misma tabla o -más habitual- en tablas distintas. Esa relación se suele establecer con la clave primaria, por lo que si esta es múltiple (varios atributos), la clave ajena también lo será.

En la siguiente imagen vemos una relación entre las tablas usuarios y solicitudes porque hemos decidido que un usuario puede realizar varias. En solicitudes tenemos una clave ajena (la columna usuario_id) que hace referencia a la clave primaria (id) de usuarios. De este modo, sabemos a qué usuario pertenece una solicitud y, por extensión, todas las realizadas por cada uno de ellos.

ejemplo tablas relacionadas

Para trabajar con bases de datos relaciones se utiliza el lenguaje SQL (Structure Query Language o lenguaje estructurado de consultas), el cual está estandarizado aunque cada base de datos añade elementos propios. Es fácil de comprender y se utiliza tanto para recuperar los datos como para crearlos, modificarlos y definir la estructura de las tablas que los contienen. Tiene la particularidad de ser un lenguaje declarativo: con él expresamos qué información queremos obtener, pero no la forma de conseguirla.

Interacción Java\bases de datos

Con una base de datos relacional resolvemos el almacenamiento de la información de nuestras aplicaciones. Ahora, nuestro objetivo será utilizarla con Java, donde además seguimos un modelo distinto (orientación a objetos). La solución es emplear SQL, utilizando como mediador entre nuestro código y la base de datos la API JDBC, un estándar incluido en la JRE que permite interactuar con cualquier base de datos relacional que proporcione una implementación, llamada driver o controlador JDBC. Con ella, nos conectarnos a la base de datos, ejecutamos sentencias SQL de cualquier tipo y obtenemos sus resultados en los casos en que sea necesario.

De nuevo, apreciamos uno de los beneficios del uso de especificaciones y APIs genéricas: con JDBC nuestro código será portable. Las incompatibilidades solo deberían surgir si usamos tipos, funciones, etc, exclusivos de una base de datos específica.

En el curso usaremos Connector/J, el driver JDBC de MySQL. Lo proporciona Oracle bajo licencia GPL 2.

Echemos un vistazo al siguiente fragmento de código. Obtiene una conexión en un bloque try-with-resources que asegura su cierre, recupera un listado de registros de una tabla con SQL y los convierte en objetos.

    public <Country> findAllPureJdbc() throws SQLException {
        String sql = "SELECT * FROM country";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql);) {
            ResultSet resultSet = ps.executeQuery();
            return mapResults(resultSet);
        } catch (SQLException ex) {
            logger.error(ex);
            throw ex;
        }
    }

    private List<Country> mapResults(ResultSet resultSet) throws SQLException {
        List<Country> results = new ArrayList<>();
        while (resultSet.next()) {
            Country country = new Country();
            country.setId(resultSet.getLong("id"))
                    .setName(resultSet.getString("name"))
                    .setPopulation(resultSet.getInt("population"))
                    .setCurrency(resultSet.getString("currency"));
            results.add(country);
        }
        return results;
    }

Demasiado código para hacer una operación tan rutinaria, y ni siquiera hemos tenido que tratar con transacciones complejas. Para cada tabla, probablemente necesitemos codificar un conjunto de operaciones denominadas CRUD (crear, obtener, actualizar, eliminar). Esto será bastante código que se repite una y otra vez con pequeñas diferencias. Una cantidad ingente de trabajo, por mucho que consigamos generalizar y reutilizar parte del código.

JPA al rescate

Variadas soluciones han ido surgiendo con el objetivo de simplificar y abstraer todo estas tareas tan repetitivas pero necesarias a la hora de interactuar con bases de datos relacionales, en el caso de Java con JDBC. A estas herramientas se les conoce con el nombre de ORM (object relational mapping ). Groso modo, partiendo de la configuración de la correspondencia entre las clases de nuestro dominio y las tablas, son capaces de realizar la transformación entre ambas. Es decir, funcionan como una especie de «traductor».

La especificación Jakarta Persistence (de ahora en adelante me referiré a ella como JPA), nace en 2006 para estandarizar las herramientas ORM que por aquel entonces ya gozaban de cierta implantación y tras el poco éxito en este campo de las especificaciones EJB 2.x y JDO. En concreto, JPA se basó en Hibernate ORM y sus anotaciones, recogiendo un conjunto de ellas suficiente para solucionar los requerimientos más frecuentes.

La siguiente línea temporal muestra la evolución de JPA (fila superior) frente a JEE y Java. Desde 2009, no ha habido grandes novedades y un par de nuevas versiones han pulido el juego de características centrales de la especificación que ya quedaron bien definidas en la versión 2.0. La 3.0 solo supone el alineamiento de JPA con Jakarta EE 9 que vimos en el primer capítulo.

Usaremos Hibernate como implementación o proveedor de JPA, pues es la proporcionada por WildFly (nótese que Red Hat\JBoss es la propietaria de ambos productos). Aunque, como es habitual, nos centraremos en el estándar, en este caso concreto resulta imprescindible estudiar el funcionamiento de la implementación. Este conocimiento es necesario para usarla de forma eficiente y, además, su API ofrece funcionalidades exclusivas que resultan irresistibles y amplían las posibilidades de JPA. El inconveniente es la pérdida de portabilidad con respecto al servidor.

Otros proveedores son Eclipse Link (implementación de referencia para JPA 2.2 ) y Apache OpenJPA. A continuación se muestra los que incluyen los servidores de aplicaciones más populares.

HibernateWildFly \ JBoss EAP
Apache OpenJPAApache TomEE
Payara Server, Eclipse GlassFish, Open LibertyEclipse Link

Con independencia de la implementación, enumero algunas ventajas del uso de JPA.

  • Las operaciones básicas de tipo CRUD ya las tenemos, solo hay que invocar el método adecuado. Nos ahorraremos código rutinario y tedioso de escribir.
  • Es independiente de la base de datos utilizada. Hibernate es compatible con una gran variedad de ellas.
  • Ofrece un lenguaje declarativo de consultas similar a SQL, llamado JPQL, en el que se usan clases y atributos en lugar de tablas y columnas.
  • Se pueden construir consultas de forma programática con JPA Criteria.
  • Facilidad para lidiar con transacciones, cachés o bloqueos (locking).
  • Gracias a su popularidad hay muchos libros, blogs y cursos disponibles. Y, por supuesto, consultas en StackOverflow.

Una maravilla, pero no es oro todo lo que reluce. Hibernate hace un trabajo fenomenal generando y ejecutando sentencias SQL a partir de los mecanismos que provee JPA para la obtención de datos (definición de entidades, JPQL, JPA Criteria). No obstante, para que estas operaciones sean eficientes, ya indiqué que debemos conocer muy bien esta herramienta. De lo contrario, terminaremos haciendo auténticas barbaridades que hundirán el rendimiento de la aplicación -¡he visto cada cosa!-. Iré subrayando los errores más comunes y ofreceré consejos y recomendaciones para usar Hibernate de forma «responsable» y productiva.

¿Dónde queda SQL?

Tengo la impresión de que algunos programadores utilizan JPA con un enfoque equivocado. Su objetivo no es reemplazar SQL, enterrándolo bajo una cómoda capa de abstracción, de tal modo que nos olvidemos por completo de su existencia y de la base de datos subyacente. De entrada, se requiere del conocimiento del modelo relacional para definir la traducción entre clases y tablas. Y, sobre todo, en ocasiones, si queremos conseguir un buen rendimiento, hay sacar partido a las funcionalidades y características de la base de datos, lo que exige cierta soltura manejando SQL.

Por ejemplo, supongamos que tenemos que leer una gran cantidad de registros, realizar algunas operaciones con ellos y actualizarlos. Si podemos hacerlo con una única sentencia SQL o un procedimiento almacenado (*), será mejor que importar los datos en nuestra aplicación, operar con ellos en Java y enviar los resultados de vuelta. La diferencia de rendimiento entre ambas estrategias, y hablo por experiencia personal, puede ser dramática, hasta tal punto que la segunda podría resultar inviable.

(*) Numerosas bases de datos relacionales, incluyendo MySQL, permiten la definición de funciones que contienen instrucciones SQL para encapsular acciones. Pueden invocarse desde aplicaciones externas, recibir parámetros y devolver resultados. Entre otras virtudes, son muy eficientes porque todo se realiza en la base de datos.

Otro caso típico es la escritura de consultas. JPQL tiene limitaciones e implementar consultas complejas de forma programática a veces es muy difícil. Ante esta situación, recurrir a SQL es lo más sencillo y práctico. Asimismo, nos permite utilizar funciones exclusivas de la base de datos, por ejemplo, para aplicar en la consulta cálculos basados en fechas que no podemos hacer de otro modo y que nos obligaría a procesar los datos en el código. En este escenario, es más eficiente obtener con SQL la información con el formato exacto que queremos.

Pensando en esta problemática, JPA ofrece soporte para usar SQL y así evitar el uso directo de JDBC. Nos beneficiaremos, entre otros, de sus mecanismos de paginación, transformación de resultados y gestión de transacciones. También podremos invocar con sencillez funciones y procedimientos almacenados. Lo veremos a su debido tiempo.

El precio a pagar es una reducción de la portabilidad de la aplicación, pero en la mayoría de proyectos no tendremos que asumir el coste: será muy raro que tengamos que cambiar de base de datos o ser compatibles con varias al mismo tiempo. Y aunque nos encontremos en esas situaciones, la portabilidad no debería ser una excusa para crear aplicaciones lentas e ineficientes cuando se trata de usar la base de datos.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.