Controlar lo que sucede cuando una operación de base de datos falla es fundamental en cualquier aplicación profesional. La anotación @Transactional de Spring permite hacer rollback automático de todas las transacciones registradas cuando ocurre un error, evitando datos inconsistentes. A continuación se explica paso a paso cómo implementarla junto con la capa de servicio.
¿Cómo crear una clase de servicio con @Service?
Antes de trabajar con transacciones, es necesario separar la lógica de negocio en una clase dedicada. En Spring Boot, el estereotipo @Service marca una clase como componente de servicio, lo que permite que el framework la gestione e inyecte automáticamente [1:05].
Dentro de la carpeta service se crea la clase UserService anotada con @Service. Lo primero es configurar un log utilizando Apache Commons Logging para registrar información durante la ejecución:
java
@Service
public class UserService {
private final Log log = LogFactory.getLog(UserService.class);
private final UserRepository userRepository;
La inyección de dependencias se realiza a nivel de constructor, pasando el UserRepository como parámetro. Este patrón garantiza que la dependencia esté disponible desde el momento en que se instancia el servicio [1:52].
¿Qué hace la anotación @Transactional y cómo se aplica?
El método saveTransactional recibe una lista de usuarios y los almacena uno por uno. Primero se utiliza un stream con peek para registrar cada usuario en el log, y luego un forEach para persistirlos mediante el repositorio [2:30].
El operador :: es una referencia a método de Java 8. En lugar de escribir una lambda como user -> userRepository.save(user), se usa userRepository::save, que tiene exactamente la misma funcionalidad pero resulta más conciso [3:18].
Al colocar @Transactional sobre el método, Spring envuelve toda la ejecución en una sola transacción de base de datos. Si el usuario uno y el usuario dos se registran correctamente pero el usuario tres produce un error, ninguno de los registros anteriores se conserva. El rollback revierte todos los cambios realizados dentro de esa transacción [3:45].
¿Cómo obtener todos los usuarios con findAll?
Para verificar qué datos quedaron almacenados, se agrega un segundo método en UserService:
java
public List<User> getAllUsers() {
return userRepository.findAll();
}
El método findAll proviene de la interfaz JpaRepository y devuelve una lista genérica de tipo T, es decir, cualquier entidad mapeada. No requiere implementación adicional [6:20].
¿Cómo probar el flujo transaccional en la aplicación?
En la clase FundamentosApplication se crea un método privado saveWithErrorTransactional donde se instancian cuatro usuarios de prueba [4:40]:
Después de guardar, se consultan todos los usuarios y se imprimen con un stream y forEach para confirmar el resultado [7:10]:
java
userService.getAllUsers()
.stream()
.forEach(user -> log.info("Usuario dentro del método transaccional: " + user));
Al ejecutar sin provocar errores, los cuatro registros aparecen correctamente en la base de datos. La verdadera ventaja de @Transactional se evidencia cuando un fallo interrumpe el proceso: todos los inserts previos se revierten automáticamente.
¿Por qué es importante separar repositorio y servicio?
El repositorio se encarga exclusivamente del acceso a datos.
El servicio contiene la lógica de negocio y coordina las operaciones.
Esta separación facilita el mantenimiento, las pruebas unitarias y la reutilización de código.
Ahora que conoces la estructura básica de @Transactional y la capa de servicio, vale la pena experimentar provocando errores intencionados para observar el rollback en acción. ¿Has tenido algún caso donde datos inconsistentes llegaron a producción? Comparte tu experiencia en los comentarios.
Para que copien y peguen el codigo para no escribirlo todo :)
User test1 =newUser("TestTransactional1","TestTransactional1@domain.com",LocalDate.now());User test2 =newUser("TestTransactional2","TestTransactional2@domain.com",LocalDate.now());User test3 =newUser("TestTransactional3","TestTransactional3@domain.com",LocalDate.now());User test4 =newUser("TestTransactional4","TestTransactional4@domain.com",LocalDate.now());
👍
Muchas gracias
¿Para qué usar una transacción?
El objetivo de una transacción es ejecutar todas las líneas de código de nuestro método y guardar finalmente la información en un repositorio, por ejemplo en nuestro caso, una base de datos. Esto se conoce como commit de nuestra transacción.
Si por alguna razón algo fallara en nuestro método de Servicio, se daría marcha atrás a los cambios realizados en la base de datos. Esto se conoce como rollback.
Lo anterior permite que nuestra información, ya sea que se una única base de datos o no, esté íntegra, y no exista posibilidad de datos corruptos por errores o fallas en la ejecución de nuestros métodos Java.
Gracias por el aporte
Gracias , excelente aporte clarisimo
Diferencia entre
javax.transaction.Transactional
y
org.springframework.transaction.annotation.Transactional
La clase tiene un fundamento, no obstante, no se puede visualizar la finalidad del "rollback", se tuvo que realizar un ejemplo en el cual fallará un registro para que se viera la marcha atrás del registro de la información.
Que diferencia hay entre crear el constructor y utilizar la anotacion @Autowired?
Hola, supongo que la diferencia es que con @Autowired vas a poder tener un constructor sin atributos en parametros, en cambio sin @Autowired te verias obligado a ingresar todas las variables que necesites inyectar dentro del constructor.
Pero como tal, no hay ninguna diferencia de inyeccion, haciendolo por cada variable a hacerlo en el constructor.
Hola, realmente en cuanto al funcionamiento no existen diferencias, con cualquiera de las inyecciones tu aplicación funcionará igual.
Sin embargo, a pesar de que el @Autowired es una de las formas permitidas por el framework y de hecho la mas elegante, NO es la mas recomendada por la comunidad.. Por varios motivos:
No permite que las instancias sean inmutables (final). Basicamente en tiempo de ejecución tu dependencia inyectada podría tener cambios (de instancia u valores siendo un problema de seguridad). Deberiamos asegurar en lo posible que los objetos se mantengan SIEMPRE como los esperamos.
Lo ideal sería declarar las inyecciones así:
private final MyRepository myRepositoryInyected;
Facilita romper el principio Single responsability de SOLID. Al inyectar dependencias de esta manera posiblemente se nos pase por alto cuantas dependencias hemos inyectado. Si tenemos muchas dependencias quiere decir que nuestra clase tiene MUCHAS responsabilidades y se debería dividir.
Se incrementa el acoplamiento de tu clase al framework. Es decir, tu clase NECESITA del framework para poder funcionar. Si se inyecta por constructor esto sería transparente.
Así que, el concejo sería que en lo preferible utilices dependencias de constructor.
Hola
Otra forma de inyectar la dependencia UserService seria mediante @Autowired sin tener que pasarla en el constructor es correcto?
Por medio de metodos set y por medio la anotación @Autowired en la dependencia.
En este caso el .peek es para ver donde iba la transacción cuando falló?
En algunos cursos he visto que la anotación @Transactional la ponen a nivel de la clase, en este caso se usa en los métodos. ¿Cuál es la diferencia de referenciarlo a nivel de clase y a nivel de método?
A alguien le funcionó el código para el jpql named parameters? a mi me salia un error en la @Query. gracias
Pregunta compañeros, el Transactional cuando se ejecuta correctamente hace un auto commit? lo digo siguiendo la logica de que cuando falla me hace un rollback
¡Sí!
La verdad no aprendí a usar la notación. Solo me la presentaron.