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.
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.
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.
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.
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.
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 CrudRepository
se 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.
En la Figura 4-8, POST
un nuevo café a la aplicación y a su base de datos.
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.
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.
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).
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
yApplicationRunner
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
);
}
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.