Curso Spring Data JPA. 2: proyecto de ejemplo

logo spring

En este capítulo construiremos el proyecto de ejemplo del curso. Dado que nos centraremos en Spring Data JPA, no debe requerir grandes conocimientos de Spring Framework y JPA. Cuanto más simple, mejor.

Índice

  1. Presentación
  2. El pom
  3. Lombok
  4. La clase @SpringBootApplication
  5. Base de datos
    1. HyperSQL embebida en memoria
    2. Tablas y registros
  6. Las entidades de JPA
  7. Bitácora con Logback
  8. Pruebas automáticas con JUnit 5
  9. Ejecución
  10. El wrapper de Maven
  11. Resumen final
  12. Código de ejemplo


Presentación

La forma más práctica y recomendable de trabajar con el vasto ecosistema de desarrollo que ofrece Spring consiste en recurrir a Spring Boot. Facilita enormemente la gestión de dependencias y la configuración de los proyectos. Además, proporciona algunas funcionalidades muy útiles.

Así pues, el proyecto de ejemplo es un proyecto Maven estándar basado en Spring Boot 3. Si no conoces la herramienta de gestión y construcción de proyectos Maven, aquí tienes una breve introducción.

Explicaré el proyecto de ejemplo para que no te pierdas; pero no profundizaré en detalles tecnológicos, ya que los conocimientos básicos los presento en otros artículos que iré indicando. Por ejemplo, si eres nuevo en el mundo Spring \ Spring Boot, antes de continuar te aconsejo la lectura del siguiente:

Todo entorno de desarrollo (IDE) para Java que se precie permite trabajar con proyectos Maven sin problema alguno. De nuevo, si no tienes experiencia en estas cuestiones, te remito a otro tutorial del blog que explica cómo importar en IntelliJ y Eclipse (los IDE que yo uso) proyectos Maven alojados en GitHub:

Nota sobre Spring Boot 2. Si todavía trabajas con versiones anteriores a Spring Boot 3 o Spring 6, ten en cuenta que los paquetes cuyo nombre empieza por jakarta antes comenzaban por javax. Esto es debido al renombrado de las especificaciones Java EE (JEE) como Jakarta EE. Lo explico en la introducción de mi curso Jakarta EE 9.

El pom

Puesto que tenemos un proyecto Maven, lo primero es analizar su pom. Para usar Spring Boot declaramos el siguiente pom padre:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.4</version>
    <relativePath/>
</parent>

Añade la configuración de algunos plugins de Maven y otros detalles, como la versión de Java. Spring Boot 3 es muy restrictivo en este aspecto porque exige Java 17. Por mi parte, suelo utilizar las jdk que publica Eclipse Adoptium. En Linux y macOS, te recomiendo Sdkman para gestionar las instalaciones de Java y otras herramientas de desarrollo.

Los starters o iniciadores (una especie de metadependencias) spring-boot-starter-data-jpa y spring-boot-starter-test indican los componentes de Spring que queremos. El primero es el correspondiente a Spring Data JPA e incluye Hibernate (versión 6) como proveedor de JPA (versión 3.1). El segundo ofrece soporte para desarrollar pruebas de integración que requieran el contexto de Spring y sus beans (*).

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
 </dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

(*) Debo aclarar el vocabulario que voy a emplear. Los beans de Spring son los objetos gestionados por el contenedor de dependencias. Este contenedor, el núcleo de Spring Framework, implementa el concepto inyección de dependencias. Dado que la interfaz ApplicationContext («contexto de la aplicación») representa al contenedor, a menudo nos referimos a este último como «contexto de Spring». En realidad este concepto abarca el contenedor, su configuración y algunos servicios disponibles para los beans, como un sistema de eventos y la posibilidad de ejecutar métodos de manera asíncrona.

¡Listo! Todo lo que necesitamos de Spring Framework en solo dos dependencias. ¿Ves algo raro en ellas? ¡Falta el número de versión! Es lo adecuado: la versión la establece el pom padre, garantizando la compatibilidad entre todas las librerías.

Lombok

Usaremos Lombok con el fin de simplificar el código, ahorrándonos, entre otros, la escritura de getters y setters. Esto es posible porque Lombok genera código rutinario boilerplate. Spring Boot contempla su uso, por lo que no hace falta indicar la versión:

 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
</dependency>

La anterior configuración es para Maven. También debemos configurar nuestro IDE para que Lombok haga su magia cuando se compile el código:

  • Eclipse: hay que descargar este .jar que contiene una aplicación autoejecutable. Si no se abre al hacer doble clic, se puede lanzar con el comando java -jar lombok.jar. Es una miniaplicación en la que indicaremos la ubicación del fichero ejecutable de Eclipse.
  • IntelliJ: es compatible con Lombok desde la versión 2020.3. En versiones anteriores, debe instalarse el plugin desde el Marketplace (File->Settings->Plugins).

Comentaré el uso de Lombok a medida que sus anotaciones aparezcan por primera vez en el curso.

La clase @SpringBootApplication

Todo proyecto Spring Boot requiere de una clase marcada con @SpringBootApplication. La nuestra es la más simple posible:

package com.danielme.springdatajpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootApp {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootApp.class, args);
    }

}

Como puedes ver, SpringBootApp contiene un método main estándar. Su cometido es ejecutar el proyecto como una aplicación Spring que disfrutará de los automatismos de Spring Boot.

Base de datos

HyperSQL embebida en memoria

La base de datos es un elemento crucial; no en vano, su explotación con Spring Data JPA es el tema del curso. Mi primera opción es MySQL, la base de datos relacional de código abierto más popular. No obstante, quiero que puedas ejecutar el proyecto sin instalar una base de datos o crear un contenedor de Docker. Como afirmé en la entradilla del capítulo, cuanto más fácil, mejor.

Por ello, he resuelto emplear HyperSQL, una de las bases de datos embebidas o empotradas soportadas por Spring Boot e Hibernate. Este tipo de bases de datos se empaquetan en la aplicación y se ejecutan dentro de ella. Esto implica que cuando la aplicación arranque o se detenga, la base de datos hará lo mismo.

Vamos a utilizar HyperSQL en modo in-memory (datos volátiles en memoria) para que todos los datos, incluyendo tablas y usuarios, se pierdan tras su detención. Así, la base de datos será un lienzo en blanco cada vez que la aplicación arranque. Otro beneficio es que la base de datos irá a la velocidad de la luz 🚀

Cuando uses MySQL o cualquier otra base de datos similar, a la hora del testeo automatizado te recomiendo Testcontainers. ¡Satisfacción garantizada!

Añadimos HyperSQL al pom:

 <dependency>
     <groupId>org.hsqldb</groupId>
     <artifactId>hsqldb</artifactId>
     <scope>runtime</scope>
 </dependency>

Los datos de conexión se definen en el fichero /src/main/resources/application.properties, un archivo estándar en el que Spring Boot centraliza la configuración de buena parte de las librerías compatibles. En esta página tienes una lista con las propiedades admitidas. También podemos añadir propiedades personalizadas, tal y como explico en Tips Spring : [BOOT] propiedades de configuración personalizadas.

Esta es la configuración que Spring Boot precisa para conectarse a una instancia de HyperSQL en memoria:

spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.url=jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

Sin embargo, podemos omitir esta configuración por ser la estándar.

Con los valores anteriores, Spring Boot creará un DataSource que provea las conexiones a la base de datos. Lo hará con HikariCP, un pool de conexiones de alto rendimiento. También configurará la integración de Spring con JPA e Hibernate, el mecanismo de transacciones y, por supuesto, Spring Data. Todo es automático; Spring Boot es genial.😎

Tablas y registros

La propiedad spring.jpa.hibernate.ddl-auto determina qué hará Hibernate durante su inicio en lo que respecta a la coherencia de las configuraciones de las clases de tipo entidad con las tablas. Aquí tienes los valores que puede tomar.

Mientras programamos, sobre todo en los primeros estadios del proyecto, es habitual usar la opción update: mantiene sincronizado el modelo de entidades con las tablas. Para conseguirlo, Hibernate ejecutará las sentencias SQL de tipo DDL (*) que considere oportunas; incluso creará las tablas si la base de datos está vacía.

(*) Lenguaje de definición de datos. Son comandos de SQL (CREATE, ALTER, DROP…) que sirven para crear y modificar la estructura de la base de datos.

Con todo, lo más seguro y flexible es gestionar la estructura de la base de datos con scripts. Haremos esto último, así que configuramos Hibernate para que cuando arranque se limite a verificar que las clases de tipo entidad son compatibles con las tablas (opción validate):

spring.jpa.hibernate.ddl-auto=validate

La consecuencia es que si Hibernate detecta algún problema, lanzará una excepción que abortará el arranque de la aplicación.

Aprovechando las convenciones de Spring Boot, ponemos en el fichero /src/main/resources/schema.sql las sentencias DDL que definen el esquema:

CREATE TABLE confederations
(
    id   BIGINT generated by default as identity (start with 1),
    name VARCHAR(255) NOT NULL UNIQUE,
    PRIMARY KEY (id)
);

CREATE TABLE countries
(
    id                       BIGINT generated by default as identity (start with 1),
    name                     VARCHAR(50) NOT NULL UNIQUE,
    population               INTEGER     NOT NULL,
    ocde                     BOOLEAN     NOT NULL,
    capital                  VARCHAR(50) NOT NULL UNIQUE,
    united_nations_admission DATE,
    confederation_id         BIGINT      NOT NULL,
    PRIMARY KEY (id)
);

Tenemos dos tablas. Almacenarán datos de países y la confederación futbolística a la que pertenecen. Veremos las columnas en breve.

En el fichero /src/main/resources/data.sql hay un juego de datos en el que se basarán los tests que escribiremos en el curso:

INSERT INTO confederations (id, name)
VALUES (1, 'UEFA');
INSERT INTO confederations (id, name)
VALUES (2, 'CONCACAF');
INSERT INTO confederations (id, name)
VALUES (3, 'CONMEBOL');
INSERT INTO confederations (id, name)
VALUES (4, 'AFC');

INSERT INTO countries (id, name, capital, population, ocde, united_nations_admission, confederation_id)
VALUES (1, 'Norway', 'Oslo', 5136700, true, '1945-11-27', 1),
       (2, 'Spain', 'Madrid', 47265321, true, '1955-12-14', 1),
       (3, 'Mexico', 'Mexico City', 115296767, true, '1945-11-7', 2),
       (4, 'Colombia', 'Bogotá', 47846160, true, '1945-11-5', 3),
       (5, 'Costa Rica', 'San José', 4586353, true, '1945-11-2', 2),
       (6, 'The Netherlands', 'Amsterdam', 17734100, true, '1945-12-10', 1),
       (7, 'Republic of Korea', 'Seoul', 51744876, true, '1991-12-17', 4),
       (8, 'The Dominican Republic', 'Santo Domingo', 10694700, false, '1945-10-24', 2),
       (9, 'Peru', 'Lima', 34294231, false, '1945-10-31', 3),
       (10, 'Guatemala', 'Guatemala City', 17263239, false, '1945-11-21', 2),
       (11, 'United States of America', 'Washington, D.C.', 331893745, true, '1945-10-24', 2),
       (12, 'Vatican City State', 'Vatican City', 453, false, null, null);

Los identificadores de los registros están en la clase DatasetConstants:

public class DatasetConstants {

    public static final Long UEFA_ID = 1L;
    public static final Long CONCACAF_ID = 2L;
    public static final Long CONMEBOL_ID = 3L;
    public static final Long AFC_ID = 4L;

...

Spring Boot ejecutará ambos scripts el orden adecuado cada vez que se inicie un nuevo contexto de Spring. Recuerda que al emplearse una base de datos in-memory los datos se perderán cuando se detenga.

Las entidades de JPA

Las dos tablas del proyecto se modelan en Java como clases de tipo entidad, concepto que explico a fondo aquí. En pocas palabras, cada una de estas clases representa una tabla (*) y sus columnas, y se configura con anotaciones. Las instancias de estas clases se denoniman entidades y se corresponde con registros de las tablas.

(*) Si bien esta afirmación no es del todo exacta (herencia, tablas secundarias), resulta cierta en la inmensa mayoría de los casos.

Esta es la clase correspondiente a la tabla countries:

@Entity
@Table(name = "countries")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Country {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @Column(nullable = false)
    private Integer population;

    @Column(nullable = false, unique = true)
    private String capital;

    private Boolean ocde;

    @Column(name = "united_nations_admission")
    private LocalDate unitedNationsAdmission;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "confederation_id")
    private Confederation confederation;

}

¡Hay un montón de anotaciones!

Las dos primeras pertenecen a JPA. @Entity establece que la clase modela una entidad de JPA, mientras que @Table especifica el nombre de la tabla representada. Si la obviamos, se asume que el nombre de la tabla coincide con el de la clase.

Las cuatro siguientes son de Lombok. Solicitan la generación de los métodos getters y setters para todos los atributos, del constructor vacío y de otro constructor que recibe todos los atributos respetando el orden en el que se declaran. En realidad, el único de estos elementos que exige JPA es el constructor vacío (y con Hibernate puede ser incluso privado). Los he añadido porque nos ayudarán a jugar con la entidades. En un proyecto real, crea solo los métodos que vayas necesitando.

Estos son los atributos y sus anotaciones:

  • El identificador, marcado con @Id. Equivale a la clave primaria de la tabla. Toda clase de entidad debe tener uno. En nuestro caso lo generará la base de datos usando el tipo de dato identidad de HyperSQL. Si quieres saber por qué es la mejor estrategia, así como otras alternativas, consulta este tutorial.
  • Propiedades de texto de valor único (unique = true) y obligatorio (no nulo, nullable = false) para los nombres del país y la capital.
  • Un lógico indica si el país es miembro de la Organización para la Cooperación y el Desarrollo Económicos (OCDE). No está anotado porque no requiere de ninguna configuración adicional.
  • La fecha de admisión en las Naciones Unidas. Nótese que el nombre de la columna en la tabla y el del atributo son distintos. En estos casos, el nombre de la columna se indica con la propiedad name de @Column.
  • Una relación perezosa de muchos a uno unidireccional con la confederación futbolística de la FIFA a la que está afiliada el país.

He aquí la clase de entidad que representa a la confederación; solo tenemos su identificador y el nombre:

@Entity
@Table(name = "confederations")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Confederation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

}

El diagrama de clases.

Bitácora con Logback

El sistema de bitácora o logging de Spring Boot se basa en la API genérica SLF4J. Cuenta con implementaciones, llamadas adaptadores, para numerosas librerías de logging. La idea es que podamos cambiar de proveedor de logging sin tener que modificar nuestro código porque en él siempre llamamos a la API de Slf4j.

Spring Boot configura de serie un sistema de logging con Logback y lo integra con Slf4j. Puesto que vamos a lo fácil, nos quedamos con Logback.

Logback se configura en el fichero /src/main/resources/logback.xml. Las configuraciones básicas se pueden realizar con las propiedades disponibles en el application.properties cuyo nombre comienza por logging. Lo común es establecer el nivel global a INFO:

logging.level.root=INFO

En lo concerniente al curso, es interesante comprobar el SQL que Hibernate envía a la base de datos con esta configuración:

#sentencias SQL
logging.level.org.hibernate.SQL=DEBUG

#parámetros consultas Hibernate 5 \ Spring Boot 2
#logging.level.org.hibernate.type=TRACE

#parámetros consultas Hibernate 6 \ Spring Boot 3
logging.level.org.hibernate.orm.jdbc.bind=TRACE

Escribimos en la bitácora con los métodos de la interfaz org.slf4j.Logger, el punto de entrada a la API genérica de Slf4j que indiqué antes. Lo más cómodo para disponer de una instancia de Logger es marcar la clase donde la necesitemos con la anotación @Slf4j de Lombok: declara un atributo estático llamado log listo para ser usado. Verás un ejemplo en la próxima sección.

Pruebas automáticas con JUnit 5

Escribiremos pruebas o tests para experimentar con las funcionalidades de Spring Data JPA que vayamos viendo. Seré breve, pues lo que debes saber se explica con detalle en este tutorial:

Las pruebas se encuentran en clases como la que sigue:

@SpringBootTest
@Slf4j
class CountryRepositoryTest {

    @Autowired
    private CountryRepository countryRepository;

    @Test
    void testRepository() {
        log.info(countryRepository.toString());
    }

}

La anotación @SpringBootTest es la clave. Indica que la clase contiene pruebas que requieren el arranque automático de un contexto de Spring orientado a la realización de pruebas. Esto permite inyectar beans, como muestran las líneas 4 y 5.

De forma predeterminada, las pruebas se desarrollan con la librería Jupiter de la plataforma de testeo JUnit 5 (*). Jupiter se integra con Spring mediante una extensión que @SpringBootTest activa por nosotros.

(*) La costumbre habitual, y así lo haré, es referirse a los test de Jupiter como «tests de JUnit 5».

Cada prueba es un método marcado con @Test. Las comprobaciones del resultado de la ejecución del código sujeto de la prueba se denominan «aserciones». Las escribiré con la librería AssertJ, incluida en el iniciador de testing, porque provee aserciones más potentes y prácticas que las proporcionadas por JUnit 5. Hablo un poco sobre ella aquí.

Ejecución

Aunque suene extraño, el proyecto no está diseñado para ser ejecutado. Lanzando el método SpringBootApp#main solo conseguimos que Spring arranque y se detenga. No tenemos, por ejemplo, una API REST que permanezca en ejecución aceptando peticiones HTTP.

El código ejecutable es el de las pruebas automatizadas. Ejecutarlas desde un IDE es sencillo: selecciona las clases o paquetes con las pruebas que quieras, muestra su menú contextual con el botón derecho del ratón, y busca una opción llamada «RUN» o similar. Es probable que tu IDE ofrezca otras alternativas, como atajos de teclado o botones en las barras de herramientas.

La siguiente captura muestra IntelliJ a la izquierda y Eclipse a la derecha.

Con Maven, ejecutamos las pruebas con la orden mvn test:test o cualquier otra que incluya esta acción, como los goals install y package.

Con independencia del procedimiento que elijas, si no consigues ejecutar las pruebas con éxito, revisa minuciosamente los mensajes de error. El problema más común es que no estés usando Java 17.

El wrapper de Maven

Aunque con un entorno de desarrollo como Eclipse o IntelliJ no se requiere nada más para ejecutar el proyecto, aparte de Java 17, he incluido el wrapper de Maven en la raíz del proyecto. Se trata de una pequeña utilidad que contiene varios ficheros y scripts con lo necesario para usar una versión concreta de Maven.

En mi opinión, el uso de este wrapper constituye una buena práctica porque junto al código fuente se distribuye la herramienta para la gestión del proyecto en su versión exacta. ¿Hay que cambiar de versión? Se modifica el wrapper y listo. Se evita andar actualizando Maven en todos los equipos donde se construye el proyecto.

Con el wrapper, podemos usar Maven desde la línea de comandos llamando al script mvnw.cmd (Windows) o mvnw (Linux).

mvnw test

La orden anterior, lanzada en una terminal situada en la carpeta raíz del proyecto, compila el proyecto y ejecuta todas las pruebas.

Resumen final

Recopilo las claves del capítulo:

  • El proyecto de ejemplo del curso es un proyecto Maven basado en Spring Boot 3 y Java 17. Puedes importarlo en cualquier IDE. También incluye el wrapper de Maven.
  • La base de datos es HyperSQL, embebida y volátil. Spring Boot la inicia con los scripts /src/main/resources/schema.sql y /src/main/resources/data.sql .
  • Spring Boot se responsabiliza de configurar e integrar HyperSQL, Spring Data, JPA e Hibernate.
  • Tenemos dos tablas relacionadas representadas por las clases de entidad (@Entity) Country y Confederation.
  • Lo más importante son las entidades porque, en esencia, escribir consultas con ellas es lo que haremos durante el resto del curso.
  • Simplificamos el código con Lombok.
  • El código que veamos en el curso se probará con tests de integración desarrollados con JUnit 5.

Código de ejemplo

El proyecto de ejemplo del curso se encuentra en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.



Otros tutoriales relacionados con Spring

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Spring Boot: Gestión de errores en aplicaciones web y REST

Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas

Spring JDBC Template: simplificando el uso de SQL

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 )

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.