Índice de temario
Día | Tema a tratar |
---|---|
1 | Definiendo objetivos |
2 | Proyecto inicial |
3 | Endpoint básico |
4 | Entidades y persistencia |
5 | Transaccionalidad |
6 | Migraciones |
7 | Manejo de excepciones |
Día 6. Haciendo el caso de uso transaccional
Bueno… Ya tenemos un caso de uso pequeño para trastear con la transaccionalidad. De hecho, este capítulo tratará la transaccionalidad desde dos perspectivas.
- Caso de uso
- Test de integración
Transacción en el caso de uso
Para hacer transaccional un caso de uso, debemos aplicar la anotación @Transactional en el sitio adecuado para que obre la magia.
En nuestro caso, en el capítulo anterior lo pusimos en el Handler, que es el caso de uso que contiene la acción. Peeero… si lanzamos cualquier excepción, veremos que la transacción sigue su curso y se persisten los datos, aunque nuestra aplicación haya fallado.
¿Cuál es el truco?. @Transactional evalúa el tipo de excepción que se ha generado, y por defecto sólo hace rollback de los errores no detectables (en resumen, RuntimeException)
Para que nuestra transacción haga rollback según nuestras excepciones, debemos indicárselo. En nuestro caso optamos por las clases que hereden de Error.class. Podríamos hilar más fino y poner exactamente las que esperamos, depende de lo que necesitemos.
package com.example.users.user.application.commands
...
@Service
@Transactional(rollbackFor = [Exception::class])
class SignUpUserHandler @Autowired constructor(
private val userRepository: UserRepository,
private val uniqueIdentifierProvider: UniqueIdentifierProvider
) {
...
}
¿Parece sencillo verdad?… pues la cosa no acaba aquí… ¿Podemos hacer transacciones anidadas y cambiar su comportamiento?. La verdad es que si. Usando el parámetro propagation.
Existen seis tipos de propagación de transacciones.
Tipo de propagación | Comportamiento |
---|---|
REQUIRED | Es el tipo por defecto. Todo se ejecuta en una transacción. Si existe una, la utiliza. Si no existe, crea una. |
SUPPORTS | Puede ir o no en una transacción. Si existe una, la usa. Si no existe, no la crea y se ejecuta sin ella. |
NOT_SUPPORTED | Siempre se ejecuta fuera de una transacción. Si existe una transacción. Se ejecuta fuera de ella. |
REQUIRES_NEW | Siempre se ejecuta en una transacción nueva. |
NEVER | Siempre se ejecuta sin una transacción. Si existe una transacción, da un error. |
MANDATORY | Siempre se ejecuta en una transacción existente. Si no existe dicha transacción, da un error. |
Así están las cosas… con estas opciones podemos cubrir todos los escenarios cuando hacemos llamadas a servicios que tienen la anotación @Transactional.
No creo que nos compliquemos tanto la vida, pero es necesario saber que existen.
Transacción en el test de integración
Ahora nos preguntamos… ¿Y qué tiene que ver los tests y las transacciones?… Muy sencillo. Se usan para mantener el estado de la base de datos consistente y limpia por cada tests de integración que realicemos.
Cuando agregamos la anotación @Transactional a nuestros tests, su comportamiento es a la inversa del esperado. En lugar de comitear los cambios, hace rollback para dejarlo como al principio para el siguiente test. Pensándolo un poco, tiene sentido. Cada test no debe depender del anterior, y cada vez debe ejecutarse con la misma información para tener siempre el mismo comportamiento.
Sin esa anotación, nuestro test fallaría la segunda vez porque ya existe un registro creado. Con lo que,agregando la anotación, los cambios no se persisten nunca y podemos lanzarlo tantas veces como queramos.
package com.example.users.user.ui.signupuser
...
@Test
@Transactional
fun `Should create a User` () {
mockMvc.perform(
post("/user/signup").param("username", "Foo"))
.andExpect(status().isCreated)
val user = userRepository.findByUsername(UserName.build("Foo"))
assertThat(user).isNotNull
if (null != user) {
assertThat(user.userName.value).isEqualTo("Foo")
}
}
}
Hay un abanico de opciones para gestionar la información en los tests usando las transacciones, pero esta es la más general e importante a tener en cuenta.
Bonus track
No voy a entrar entrar en cómo alimentar la base de datos en los tests. En este caso la de Spring habla por si sola. Te da herramientas para ejecutar scripts de SQL.
Links y recursos
Transaccionalidad en código
https://www.cloudflight.io/tech/spring-transactional-no-rollback-1691/
https://www.javainuse.com/spring/boot-rollback
https://www.javainuse.com/spring/boot-transaction-propagation
Transaccionalidad en tests
https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx
Share this post
Twitter
Facebook
Reddit
LinkedIn
Email