3 minute read

Í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.


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


comments powered by Disqus