Capítulo 4. Añadir acceso a bases de datos atu aplicación Spring Boot

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Como se ha comentado en el capítulo anterior, las aplicaciones suelen exponer API sin estado por muchas y muy buenas razones. Entre bastidores, sin embargo, muy pocas aplicaciones útiles son totalmente efímeras; normalmente se almacena algún tipo de estado para algo. Por ejemplo, cada solicitud al carrito de la compra de una tienda online bien puede incluir su estado, pero una vez realizado el pedido, se conservan los datos de ese pedido. Hay muchas formas de hacer esto, y muchas formas de compartir o encaminar estos datos, pero invariablemente hay una o más bases de datos implicadas en casi todos los sistemas de tamaño suficiente.

En este capítulo, demostraré cómo añadir acceso a bases de datos a la aplicación Spring Boot creada en el capítulo anterior. Este capítulo pretende ser una breve introducción a las capacidades de datos de Spring Boot, y los capítulos siguientes profundizarán mucho más. Pero en muchos casos, los fundamentos aquí tratados siguen siendo válidos y proporcionan una solución totalmente suficiente. Vamos a profundizar.

Comprobación del código

Para empezar, consulta la rama chapter4begin del repositorio de código.

Preparar Autoconfig para el acceso a la base de datos

Como se ha demostrado antes, Spring Boot pretende simplificar al máximo el llamado caso de uso del 80-90%: los patrones de código y proceso que los desarrolladores hacen una y otra y otra vez. Una vez identificados los patrones, Boot entra en acción para inicializar los beans necesarios automáticamente, con configuraciones por defecto sensatas. Personalizar una capacidad es tan sencillo como proporcionar uno o varios valores de propiedad o crear una versión adaptada de uno o varios beans; una vez que autoconfig detecta los cambios, retrocede y sigue las directrices del desarrollador. El acceso a la base de datos es un ejemplo perfecto.

¿Qué esperamos ganar?

En nuestra anterior aplicación de ejemplo, utilicé un ArrayList para almacenar y mantener nuestra lista de cafés. Este enfoque es bastante sencillo para una sola aplicación, pero tiene sus inconvenientes.

En primer lugar, no es resistente en absoluto. Si tu aplicación o la plataforma que la ejecuta fallan, todos los cambios realizados en la lista mientras la aplicación estaba en funcionamiento -ya sea durante segundos omeses- desaparecen.

En segundo lugar, no escala. Si inicias otra instancia de la aplicación, esa segunda instancia (o las siguientes) tendrá su propia lista de cafés. Los datos no se comparten entre las múltiples instancias, por lo que los cambios en los cafés realizados por una instancia -nuevos cafés, eliminaciones, actualizaciones- no son visibles para nadie que acceda a una instancia de aplicación diferente.

Está claro que ésta no es forma de dirigir un ferrocarril.

En los próximos capítulos trataré algunas formas diferentes de resolver completamente estos problemas tan reales. Pero por ahora, vamos a sentar algunas bases que servirán como pasos útiles en el camino hacia allí.

Añadir una dependencia de base de datos

Para acceder a una base de datos desde tu aplicación Spring Boot, necesitas algunas cosas:

  • Una base de datos en funcionamiento, ya sea iniciada por/incorporada en tu aplicación o simplemente accesible a tu aplicación

  • Controladores de base de datos que permiten el acceso programático, normalmente proporcionados por el proveedor de la base de datos

  • Un módulo Spring Data para acceder a la base de datos de destino

Algunos módulos de Spring Data incluyen los controladores de base de datos adecuados como una única dependencia seleccionable desde Spring Initializr. En otros casos, como cuando Spring utiliza Java Persistence API (JPA) para acceder a almacenes de datos compatibles con JPA, es necesario elegir la dependencia JPA de Spring Data y una dependencia para el controlador específico de la base de datos de destino, por ejemplo, PostgreSQL.

Para dar el primer paso de las construcciones de memoria a la base de datos persistente, empezaré añadiendo dependencias, y por tanto capacidades, al archivo de compilación de nuestro proyecto.

H2 es una base de datos rápida escrita completamente en Java que tiene algunas características interesantes y útiles. Para empezar, es compatible con JPA, por lo que podemos conectar nuestra aplicación a ella del mismo modo que lo haríamos a cualquier otra base de datos JPA como Microsoft SQL, MySQL, Oracle o PostgreSQL. También tiene modos en memoria y en disco. Esto nos permite algunas opciones útiles después de convertir nuestro ArrayList en memoria a una base de datos en memoria: podemos cambiar H2 a persistencia basada en disco o -ya que ahora estamos utilizando una base de datos JPA- cambiar a una base de datos JPA diferente. Cualquiera de las dos opciones se simplifica mucho en ese punto.

Para que nuestra aplicación pueda interactuar con la base de datos H2, añadiré las dos dependencias siguientes a la sección <dependencies> del pom.xml de nuestro proyecto:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
Nota

El ámbito de la dependencia del controlador de base de datos H2 de runtime indica que estará presente en el classpath de tiempo de ejecución y de prueba, pero no en el classpath de compilación. Es una buena práctica para las bibliotecas que no son necesarias para la compilación.

Una vez que guardes tu pom.xml actualizado y (si es necesario) reimportes/actualices tus dependencias de Maven, tendrás acceso a la funcionalidad incluida en las dependencias añadidas. A continuación, es hora de escribir un poco de código para utilizarla.

Añadir código

Como ya tenemos código para gestionar los cafés de alguna manera, tendremos que refactorizar un poco mientras añadimos nuestras nuevas capacidades de base de datos. Creo que el mejor lugar para empezar es con la(s) clase(s) del dominio, en este caso, Coffee.

La @Entidad

Como ya he mencionado, H2 es una base de datos compatible con JPA, así que añadiré anotaciones JPA para conectar los puntos. A la propia clase Coffee le añado una anotación @Entity de javax.persistence que indica que Coffee es una entidad persistente, y a la variable miembro existente id, le añado la anotación @Id (también de javax.persistence) para marcarla como campo ID de la tabla de la base de datos.

Nota

Si el nombre de la clase -Coffee en este caso- no coincide con el nombre de la tabla de la base de datos deseada, la anotación @Entity acepta un parámetro name para especificar el nombre de la tabla de datos que coincida con la entidad anotada.

Si tu IDE es lo suficientemente útil, puede informarte de que aún falta algo en la clase Coffee. Por ejemplo, IntelliJ subraya el nombre de la clase en rojo y proporciona la útil ventana emergente al pasar el ratón por encima que se muestra en la Figura 4-1.

sbur 0401
Figura 4-1. Falta un constructor en la clase JPA Coffee

La API de Persistencia de Java requiere un constructor sin argumentos para utilizarlo al crear objetos a partir de filas de tablas de bases de datos, así que lo añadiré a continuación. Esto da lugar a la siguiente advertencia del IDE, como se muestra en la Figura 4-2: para tener un constructor sin argumentos, debemos hacer que todas las variables miembro sean mutables, es decir, no finales.

sbur 0402
Figura 4-2. Con un constructor sin carga, id no puede ser final

Eliminar la palabra clave final de la declaración de la variable miembro id lo resuelve. Hacer mutable id también requiere que nuestra clase Coffee tenga un método mutador para id para que JPA pueda asignar un valor a ese miembro, así que añado también el método setId(), como se muestra en la Figura 4-3.

sbur 0403
Figura 4-3. El nuevo método setId()

El Depósito

Con Coffee definida ahora como una entidad JPA válida capaz de ser almacenada y recuperada, es hora de establecer la conexión con la base de datos

Para ser un concepto tan sencillo, configurar y establecer una conexión a una base de datos en el ecosistema Java ha sido durante mucho tiempo un asunto bastante engorroso. Como se mencionó en el Capítulo 1, utilizar un servidor de aplicaciones para alojar una aplicación Java exigía a los desarrolladores realizar varios pasos tediosos sólo para tener las cosas listas. Una vez que empezabas a interactuar con la base de datos, o si accedías a un almacén de datos directamente desde una utilidad Java o una aplicación cliente, se esperaba que realizaras pasos adicionales que implicaban las API PersistenceUnit, EntityManagerFactory, y EntityManager (y posiblemente objetos DataSource ), abrir y cerrar la base de datos, y mucho más. Es mucha ceremonia repetitiva para algo que los desarrolladores hacen tan a menudo.

Spring Data introduce el concepto de repositorios. Un Repository es una interfaz definida en Spring Data como una abstracción útil sobre varias bases de datos. Existen otros mecanismos para acceder a bases de datos desde Spring Data que se explicarán en capítulos posteriores, pero los distintos sabores de Repository son posiblemente los más útiles en la mayoría de los casos.

Repository es un mero marcador de posición para los siguientes tipos:

  • El objeto almacenado en la base de datos

  • El campo ID único/clave principal del objeto

Hay muchas más cosas sobre los repositorios, por supuesto, y trataré mucho de ello en el Capítulo 6. Por ahora, centrémonos en dos que son directamente relevantes para nuestro ejemplo actual: CrudRepository y JpaRepository.

¿Recuerdas mi mención anterior a la práctica preferida de escribir código para utilizar la interfaz de más alto nivel adecuada al propósito? Mientras que JpaRepository amplía un puñado de interfaces y, por tanto, incorpora una funcionalidad más amplia, CrudRepository cubre todas las capacidades CRUD clave de y es suficiente para nuestra (hasta ahora) sencilla aplicación.

Lo primero que hay que hacer para habilitar el soporte de repositorios para nuestra aplicación es definir una interfaz específica para nuestra aplicación ampliando una interfaz de Spring Data Repository: .interfaceCoffeeRepo

interface CoffeeRepository extends CrudRepository<Coffee, String> {}
Nota

Los dos tipos definidos son el tipo de objeto a almacenar y el tipo de su identificador único.

Esto representa la expresión más simple de la creación de repositorios dentro de una app Spring Boot. Es posible, y muy útil a veces, definir consultas para un repositorio; también me sumergiré en ello en un capítulo futuro. Pero aquí está la parte "mágica": La autoconfiguración de Spring Boot tiene en cuenta el controlador de la base de datos en el classpath (en este caso, H2), la interfaz del repositorio definida en nuestra aplicación y la definición de la clase Coffee de la entidad JPA, y crea un bean proxy de la base de datos en nuestro nombre. No hay necesidad de escribir líneas de boilerplate casi idénticas para cada aplicación cuando los patrones son así de claros y coherentes, lo que libera al desarrollador para trabajar en nuevasfuncionalidades solicitadas.

La utilidad, también conocida como "Primavera" en acción

Ahora a poner ese repositorio a trabajar. Abordaré esto paso a paso como en capítulos anteriores, introduciendo primero la funcionalidad y puliendo después.

En primer lugar, autoconectaré/inyectaré el bean repositorio en RestApiDemoController para que el controlador pueda acceder a él cuando reciba peticiones a través de la API externa, como se muestra en la Figura 4-4.

Primero declaro la variable miembro con:

private final CoffeeRepository coffeeRepository;

A continuación, lo añado como parámetro al constructor con:

public RestApiDemoController(CoffeeRepository coffeeRepository){}
Nota

Antes de Spring Framework 4.3, era necesario en todos los casos añadir la anotación @Autowired encima del método para indicar cuándo un parámetro representaba un frijol Spring que debía autocontruirse/inyectarse. A partir de la versión 4.3, una clase con un único constructor no necesita la anotación para los parámetros autoconectados, lo que supone un útil ahorro de tiempo.

sbur 0404
Figura 4-4. Repositorio de autocableado en RestApiDemoController

Con el repositorio en su sitio, elimino la variable miembro List<Coffee> y cambio la población inicial de esa lista en el constructor para guardar en su lugar los mismos cafés en el repositorio, como en la Figura 4-4.

Según la Figura 4-5, la eliminación de la variable coffees marca inmediatamente todas las referencias a ella como símbolos irresolubles, por lo que la siguiente tarea es sustituirlos por interacciones apropiadas del repositorio.

sbur 0405
Figura 4-5. Sustitución de la variable miembro coffees desmontada

Como simple recuperación de todos los cafés sin parámetros, el método getCoffees() es un buen punto de partida. Utilizando el método findAll() incorporado en CrudRepository, ni siquiera es necesario cambiar el tipo de retorno de getCoffees(), ya que también devuelve un tipo Iterable; basta con llamar a coffeeRepository.findAll() y devolver su resultado para hacer el trabajo, como se muestra aquí:

@GetMapping
Iterable<Coffee> getCoffees() {
    return coffeeRepository.findAll();
}

Refactorizar el método getCoffeeById() presenta algunas ideas sobre lo mucho más sencillo que puede ser tu código, gracias a la funcionalidad que aportan los repositorios. Ya no tenemos que buscar manualmente en la lista de cafés un id que coincida; el método findById() de CrudRepositoryse encarga de ello por nosotros, como se demuestra en el siguiente fragmento de código. Y como findById() devuelve un tipo Optional, no es necesario hacer ningún cambio en la firma de nuestro método:

@GetMapping("/{id}")
Optional<Coffee> getCoffeeById(@PathVariable String id) {
    return coffeeRepository.findById(id);
}

Convertir el método postCoffee() para que utilice el repositorio también es una tarea bastante sencilla, como se muestra aquí:

@PostMapping
Coffee postCoffee(@RequestBody Coffee coffee) {
    return coffeeRepository.save(coffee);
}

Con el método putCoffee(), vemos de nuevo algunas de las importantes funciones de ahorro de tiempo y código de CrudRepository. Utilizo el método incorporado en el repositorio existsById() para determinar si se trata de un Coffee nuevo o existente y devolver el código de estado HTTP apropiado junto con el Coffee guardado, como se muestra en este listado:

@PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                 @RequestBody Coffee coffee) {

    return (!coffeeRepository.existsById(id))
            ? new ResponseEntity<>(coffeeRepository.save(coffee),
                  HttpStatus.CREATED)
            : new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.OK);
}

Por último, actualizo el método deleteCoffee() para que utilice el método deleteById() incorporado en CrudRepository, como se muestra aquí:

@DeleteMapping("/{id}")
void deleteCoffee(@PathVariable String id) {
    coffeeRepository.deleteById(id);
}

Aprovechar un bean de repositorio creado mediante la API fluida de CrudRepository agiliza el código de RestApiDemoController y lo hace mucho más claro, tanto en términos de legibilidad como de comprensibilidad, como demuestra el listado completo de código:

@RestController
@RequestMapping("/coffees")
class RestApiDemoController {
    private final CoffeeRepository coffeeRepository;

    public RestApiDemoController(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;

        this.coffeeRepository.saveAll(List.of(
                new Coffee("Café Cereza"),
                new Coffee("Café Ganador"),
                new Coffee("Café Lareño"),
                new Coffee("Café Três Pontas")
        ));
    }

    @GetMapping
    Iterable<Coffee> getCoffees() {
        return coffeeRepository.findAll();
    }

    @GetMapping("/{id}")
    Optional<Coffee> getCoffeeById(@PathVariable String id) {
        return coffeeRepository.findById(id);
    }

    @PostMapping
    Coffee postCoffee(@RequestBody Coffee coffee) {
        return coffeeRepository.save(coffee);
    }

    @PutMapping("/{id}")
    ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                     @RequestBody Coffee coffee) {

        return (!coffeeRepository.existsById(id))
                ? new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.CREATED)
                : new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    void deleteCoffee(@PathVariable String id) {
        coffeeRepository.deleteById(id);
    }
}

Ahora sólo nos queda comprobar que nuestra aplicación funciona como esperábamos y que la funcionalidad externa sigue siendo la misma.

Nota

Un enfoque alternativo para probar la funcionalidad -y una práctica recomendada- es crear primero pruebas unitarias, a lo Test Driven Development (TDD). Recomiendo encarecidamente este enfoque en entornos de desarrollo de software del mundo real, pero he descubierto que cuando el objetivo es demostrar y explicar conceptos discretos de desarrollo de software, menos es mejor; mostrar lo menos posible para comunicar claramente conceptos clave aumenta la señal y disminuye el ruido, aunque el ruido sea útil más adelante. Como tal, cubro las pruebas en un capítulo dedicado más adelante en este libro.

Guardar y recuperar datos

Una vez más a la brecha, queridos amigos, una vez más: accediendo a la API desde la línea de comandos mediante HTTPie. La consulta al punto final cafés da como resultado los mismos cuatro cafés devueltos desde nuestra base de datos H2 que antes, como se muestra en la Figura 4-6.

Si copias el campo id de uno de los cafés que acabamos de enumerar y lo pegas en una solicitud GET específica para un café, obtendrás el resultado que se muestra en la Figura 4-7.

sbur 0406
Figura 4-6. GET-tando todos los cafés
sbur 0407
Figura 4-7. GET-tando un café

En la Figura 4-8, POST un nuevo café a la aplicación y a su base de datos.

sbur 0408
Figura 4-8. POST-Añadir un nuevo café a la lista

Como se ha comentado en el capítulo anterior, un comando PUT debe permitir actualizar un recurso existente o añadir uno nuevo si el recurso solicitado aún no existe. En la Figura 4-9, especifico el id del café que acabo de añadir y paso al comando un objeto JSON con un cambio en el nombre de ese café. Tras la actualización, el café con el id de "99999" tiene ahora un name de "Caribou Coffee" en lugar de "Kaldi's Coffee", y el código de retorno es 200 (OK), como era de esperar.

sbur 0409
Figura 4-9. PUT-Cómo actualizar un café existente

A continuación, inicio una petición similar a PUT pero especificando en el URI un id que no existe. La aplicación añade un nuevo café a la base de datos de acuerdo con el comportamiento especificado por la IETF y devuelve correctamente un estado HTTP 201 (Creado), como se muestra en la Figura 4-10.

sbur 0410
Figura 4-10. PUT-creando un nuevo café

Por último, compruebo la eliminación de un café especificado enviando una solicitud a DELETE, que sólo devuelve un código de estado HTTP 200 (OK), indicando que el recurso se ha eliminado correctamente y nada más, puesto que el recurso ya no existe, según la Figura 4-11. Para comprobar nuestro estado final, volvemos a consultar la lista completa de cafés(Figura 4-12).

sbur 0411
Figura 4-11. DELETE-ing un café
sbur 0412
Figura 4-12. GET-Ahora todos los cafés de la lista

Como antes, ahora tenemos un café adicional que no estaba inicialmente en nuestro repositorio: Café de aceite Mötor.

Un poco de pulido

Como siempre, hay muchas áreas que podrían beneficiarse de una atención adicional, pero me limitaré a dos: la extracción de la población inicial de datos de muestra a un componente separado y un poco de reordenación de las condiciones para mayor claridad.

En el capítulo anterior poblé la lista de cafés con algunos valores iniciales en la clase RestApiDemoController, así que mantuve esa misma estructura -hasta ahora- en este capítulo tras convertirla a una base de datos con acceso a repositorios. Una práctica mejor es extraer esa funcionalidad a un componente independiente que se pueda activar o desactivar rápida y fácilmente.

Hay muchas formas de ejecutar código automáticamente al iniciar la aplicación, como utilizar una clase CommandLineRunner o ApplicationRunner y especificar una lambda para lograr el objetivo deseado: en este caso, crear y guardar datos de muestra. Pero prefiero utilizar una clase @Component y un método @PostConstruct para conseguir lo mismo por las siguientes razones:

  • Cuando los métodos que producen beans CommandLineRunner y ApplicationRunner autoconectan un bean de repositorio, las pruebas unitarias que simulan el bean de repositorio dentro de la prueba (como suele ser el caso) se rompen.

  • Si simulas el bean repositorio dentro de la prueba o deseas ejecutar la aplicación sin crear datos de muestra, es rápido y fácil desactivar el bean que rellena los datos reales simplemente comentando su anotación @Component.

Recomiendo crear una clase DataLoader similar a la que se muestra en el siguiente bloque de código. Extraer la lógica para crear datos de muestra al método loadData() de la clase DataLoader y anotarlo con @PostContruct restaura RestApiDemoController a su propósito único de proporcionar una API externa y hace que DataLoader sea responsable de su propósito previsto (y obvio):

@Component
class DataLoader {
    private final CoffeeRepository coffeeRepository;

    public DataLoader(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;
    }

    @PostConstruct
    private void loadData() {
        coffeeRepository.saveAll(List.of(
                new Coffee("Café Cereza"),
                new Coffee("Café Ganador"),
                new Coffee("Café Lareño"),
                new Coffee("Café Três Pontas")
        ));
    }
}

La otra pizca de pulido es un ajuste ciertamente pequeño de la condición booleana del operador ternario dentro del método putCoffee(). Después de refactorizar el método para utilizar un repositorio, no queda ninguna justificación convincente para evaluar primero la condición negativa. Eliminar el operador no (!) de la condición mejora ligeramente la claridad; por supuesto, es necesario intercambiar los valores verdadero y falso del operador ternario para mantener los resultados originales, como se refleja en el código siguiente:

@PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                 @RequestBody Coffee coffee) {

    return (coffeeRepository.existsById(id))
            ? new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.OK)
            : new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.CREATED);
}

Comprobación del código

Para ver el código completo del capítulo, consulta la rama chapter4end del repositorio de código.

Resumen

En este capítulo se ha mostrado cómo añadir acceso a bases de datos a la aplicación Spring Boot creada en el capítulo anterior. Aunque pretendía ser una introducción concisa a las capacidades de datos de Spring Boot, proporcioné una visión general de lo siguiente:

  • Acceso a bases de datos Java

  • La API de Persistencia de Java (JPA)

  • La base de datos H2

  • Spring Data JPA

  • Repositorios de Datos Spring

  • Mecanismos para crear datos de muestra mediante repositorios

En capítulos posteriores se profundizará mucho más en el acceso a bases de datos de Spring Boot, pero los fundamentos que se tratan en este capítulo proporcionan una base sólida sobre la que construir y, en muchos casos, son suficientes por sí solos.

En el próximo capítulo, hablaré y demostraré herramientas útiles que Spring Boot proporciona para obtener información sobre tus aplicaciones cuando las cosas no funcionan como esperabas o cuando necesitas verificar que lo hacen.

Get Spring Boot: En marcha now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.