El tratar transacciones con Spring e Hibernate es una de las casuísticas más utilizadas por los proyectos de programación y, aun así, continúa siendo una de las menos conocidas.
Introducción
Una transacción de base de datos es un conjunto de instrucciones ejecutadas en bloque. Por ejemplo, realizo una consulta, modifico un registro A en la base de datos y elimino un registro B. Si se produce un error en alguna de estas instrucciones, todo el proceso se revierte. Así, si luego consulto la base de datos, observaré que el registro A no ha sido alterado. A este proceso de revertir las instrucciones realizadas se le denomina «hacer un rollback«, mientras que el proceso de confirmar todas las instrucciones en bloque, una vez comprobado que no se ha producido ningún error, se le llama «hacer un commit«.
Las transacciones se inician y finalizan a nivel de servicio, nunca a nivel de DAO (Objeto de Acceso a Datos, por sus siglas en inglés). Esto tiene sentido, ya que el servicio es el encargado de gestionar toda la lógica de negocio: llama a los DAOs necesarios para consultar, guardar o modificar registros y debe hacerlo de manera atómica. Por ejemplo, consideremos un proceso batch que opera durante la noche y se encarga de recalcular las hipotecas de los clientes. En el primer paso, obtiene el registro de la hipoteca; en el segundo, recalcula la hipoteca; y en el tercero, actualiza el registro del cliente con una marca para indicar que el proceso ya ha sido ejecutado para ese cliente. El DAO se ocupa de la consulta, de la actualización de la hipoteca y de la actualización del registro con la marca de manera individual: primero con un método que ejecute una consulta, luego con una actualización de la hipoteca y, posteriormente, con una actualización en la tabla de avisos del cliente. Sin embargo, es el servicio, o su equivalente en procesos batch, el que asegura que todo este proceso se maneje como una unidad. Si la transacción comenzase a nivel del DAO, no se podría controlar la atomicidad de todo el proceso o, peor aún, se tendría que trasladar la lógica de negocio a la capa del DAO.
Además, con JPA/Hibernate, cada vez que deseamos realizar una modificación en la base de datos, necesitamos una transacción activa. Esto se aplica incluso si solo vamos a insertar un dato y no un bloque de acciones.
Transacciones con Spring
La manera más común de gestionar las transacciones en Spring es mediante la anotación @Transactional
en la cabecera del método de una clase (nunca de una interfaz) administrada por Spring:
@Transactional
public void realizaAccionTransaccionalmente() {
// Soy transaccional.
}
Además, es necesario incluir la propiedad <tx:annotation-driven>
en el archivo de contexto de Spring.
Sin embargo, la gestión de transacciones no es tan sencilla como parece. Spring opera con los beans a través de proxies, lo que añade complejidad al asunto. Aquí no entraremos en detalle sobre qué es un proxy, pero es importante señalar que, debido a esto, la anotación solo funcionará en métodos públicos que accedan a la clase. Por lo tanto:
@Transactional
private void realizaAccionTransaccionalmente() {
// No soy transaccional a pesar de la anotación.
}
no funcionará porque el método es privado. Lo peor es que no se avisará del error, ya que @Transactional
se trata como metadato: se utiliza si es aplicable, pero se ignora en caso contrario.
Otro ejemplo que tampoco funcionará:
@Transactional
protected void realizaAccionTransaccionalmente() {
// No soy transaccional a pesar de la anotación.
}
En este caso, el método tampoco es público.
Veamos un escenario que puede causar confusiones:
public class A {
@Autowired
private B b;
public void ejecutaAccionEnB() {
b.realizaAccion();
}
}
public class B {
public void realizaAccion() {
realizaAccionTransaccionalmente();
}
@Transactional
public void realizaAccionTransaccionalmente() {
// ¿Soy transaccional?
}
}
Aquí, aunque el método realizaAccionTransaccionalmente
esté marcado con @Transactional
, no se iniciará una transacción porque se accedió a él a través de un método no transaccional (realizaAccion
).
Por el contrario:
public class A {
@Autowired
private B b;
public void ejecutaAccionEnB() {
b.realizaAccion();
}
}
public class B {
@Transactional
public void realizaAccion() {
realizaAccionTransaccionalmente();
}
public void realizaAccionTransaccionalmente() {
// Solo soy transaccional si se me llama
// desde el método realizaAccion.
}
}
En este caso, la transacción se activa correctamente porque @Transactional
está sobre el método de entrada a la clase.
Es crucial tener en cuenta este detalle sobre los métodos de entrada, ya que puede causar frustraciones. La regla es clara: la anotación solo tiene efecto en la cabecera de un método a través del cual se accede a la clase. Aunque es posible marcar todos los métodos o incluso la clase entera como transaccionales, esto puede llevar a problemas, por lo que es recomendable proceder con cautela.
Ámbito de la transacción
Es esencial entender la duración de una transacción, es decir, cuándo comienza y termina. Si un método está anotado con @Transactional
, la transacción comenzará justo antes de la primera línea del método y terminará después de la última. Si este método llama a otros, estas llamadas se ejecutan dentro de la misma transacción sin necesidad de anotarlos con @Transactional
.
Cuando un método anotado con @Transactional
invoca a otro método que también está anotado con @Transactional
, entra en juego el concepto de propagación de transacción. Por defecto, la transacción tiene una propagación de PROPAGATION_REQUIRED
, que significa que si no existe una transacción, se crea una nueva; si ya existe, se aprovecha la existente. En el ejemplo proporcionado, el primer método abre la transacción y el segundo la utiliza, manteniendo ambas operaciones dentro de la misma transacción.
Si se desea que el método de la clase B ejecute una nueva transacción, permitiendo que, si algo falla en B, no afecte a la transacción de A, es necesario modificar la propagación en B como se muestra a continuación:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hazAlgoB() {
hazAlgoTransaccionalmente();
}
Con esta configuración, al entrar en B se generará una nueva transacción.
Spring soporta varios tipos de propagación de transacciones:
- PROPAGATION_REQUIRED: Comportamiento predeterminado. Utiliza la transacción existente o crea una nueva si no hay ninguna.
- REQUIRES_NEW: Inicia una nueva transacción, suspendiendo cualquier transacción existente hasta que la nueva transacción finalice.
- PROPAGATION_SUPPORTS: Ejecuta dentro de una transacción si ya existe una; de lo contrario, se ejecuta fuera de una transacción.
- PROPAGATION_MANDATORY: Exige que exista una transacción; si no hay ninguna, lanza una excepción.
- PROPAGATION_NEVER: Asegura que no se ejecute dentro de una transacción; si hay una transacción activa, lanza una excepción.
- PROPAGATION_NOT_SUPPORTED: Ejecuta fuera de la transacción existente; cualquier transacción activa se suspende hasta que finalice el método.
Cada una de estas opciones de propagación permite un control detallado sobre cómo y cuándo se deben manejar las transacciones dentro de aplicaciones Spring, permitiendo ajustar el comportamiento transaccional a los requisitos específicos de cada situación.
Errores comunes con @Transactional
Transacción obligatoria
Un malentendido frecuente es pensar que se necesita una transacción para realizar cualquier operación con la base de datos, incluyendo consultas simples. Aunque técnicamente Hibernate maneja estas operaciones bajo una transacción implícita, desde el punto de vista del desarrollador, no es necesario marcar explícitamente las consultas de solo lectura con @Transactional
. Esto se debe a que Hibernate puede abrir y manejar transacciones de manera transparente, incluso si no se indica explícitamente.
Transacciones read-only
Otra confusión es la propiedad readonly. Si ponemos
@Transactional(readonly=true)
public void hazAlgoTransaccionalmente() {
// Soy transaccional!
}
¿Qué se supone que hace? Según la documentación, una transacción readonly puede ser utilizada cuando quieres que tu código lea, pero no modifique ningún dato y deja intuir que puede ser beneficioso en términos de rendimiento.
Ahora bien, ¿cómo afecta a esto a la hora de hacer inserciones y/o actualizaciones? Supongamos el siguiente código lanzado con JUnit sobre una base de datos HSQLDB:
@Test
@Transactional(readonly=true)
public void insertTest() {
List resultados = genericDao.getAll(Persona.class);
Assert.assertEquals(3, resultados.size());
Persona p = new Persona();
p.setNombre("Test");
p.setEdad(33);
Assert.assertEquals(0, p.getId());
genericDao.insert(p);
resultados = genericDao.getAll(Persona.class);
Assert.assertEquals(4, resultados.size());
}
¿Qué sucede? La verdad es que esto tiene algo de trampa. Uno puede pensar que si he marcado la transacción como readonly la inserción de la persona no debería hacerse y el test fallaría. Pero no, Hibernate pasa del readonly e inserta el registro sin problema. Otros pensarán: No, que no te enteras, lo que sucede es que la entidad persona queda marcada como de solo lectura y lo que no puedo es modificarla y luego hacer un update. Pues tampoco, le cambio la edad y para la base de datos. Los más avispados pueden pensar, lo que sucede es que no haces un flush antes de la query. Vale, aceptamos barco. Eso sí que es cierto. O sea, que parece que la documentación no es correcta y el readonly prácticamente no hace nada.
El problema es cuando sales del test y vas a producción y el comportamiento es diferente y empiezan a saltarte errores. WTF! ¿Qué sucede aquí? ¡Si mis tests funcionan perfectamente! Pues sucede que el parámetro readonly es dependiente de a) la implementación JPA y b) el dirver jdbc que estés utilizando.
Por ejemplo, el test anterior lo lancé en una base de datos HSQLDB, pero el comportamiento en Oracle puede ser diferente. Con Oracle 11 sí que puede generar una transacción de solo lectura, con HSQLDB no existe esa posibilidad. Ojo, esto no pasa en todas las versiones de Oracle, ya que depende del driver que utilices. Si el driver sobreescribe correctamente el método setReadOnly del interfaz Connection tendremos una transacción readonly. Si no lo tenemos, pues solo tendremos que no nos hace un flush antes de una query.
Y lo malo de todo esto es que no está documentado en ninguna parte. Para que uno se fíe de la documentación. Así que ojo.
Modificar la transacción
Intentar cambiar los parámetros de una transacción después de haber sido abierta es un error común. Por ejemplo, algo que ya he visto alguna vez:
public class A {
@Autowired
private B b;
public void hazAlgoA() {
b.hazAlgoB();
}
}
class B {
@Autowired
private C c;
@Transactional
public void hazAlgoB() {
// inserto un dato en bbdd
try {
c.hazAlgoC();
catch( NullPointerExecption e) {
}
// inserto otro dato en bbdd
}
}
class C {
@Transactional(readonly=true)
public void hazAlgoC() {
// leo un dato en bbdd
}
}
Se abre una transacción para B se llama a C y cómo se quiere hacer una lectura se marca la transacción como readonly. Bueno, esto no es posible; una vez que una transacción está abierta, sus parámetros no pueden ser modificados hasta que se complete.
Commit y Rollback
Bueno, ya lo tenemos (casi) todo claro. Ahora lo que queremos es controlar el commit o el rollback de nuestra transacción. La cosa es simple, si un método anotado como @Transaccional lanza una excepción que herede de RuntimeException se producirá un rollback. En caso contrario, un commit.
Vamos a dar vueltas a esto. Primero lo más evidente:
public class A {
@Autowired
private B b;
public void hazAlgoA() {
try {
b.hazAlgoB();
}
catch( NullPointerExecption e) {
}
}
}
public class B {
@Transactional
public void hazAlgo(Entidad entidad) {
// inserto un dato en bbdd
throw new NullPointerException();
// inserto otro dato en bbdd
}
}
lanza una NullPointerException, que hereda de RuntimeException, luego se produce un rollback y la primera inserción no se realiza (ni la segunda, claro).
Veamos esto
public class A {
@Autowired
private B b;
public void hazAlgoA() {
try {
b.hazAlgoB();
}
catch( FileNotFoundException e) {
}
}
}
public class B {
@Transactional
public void hazAlgo(Entidad entidad) {
// inserto un dato en bbdd
throw new FileNotFoundException();
// inserto otro dato en bbdd
}
}
¿Qué hará? ¿Inserta un dato? ¿Dos? ¿Ninguno? Veamos, FileNotFoundException no hereda de RuntimeException por lo que no debería hacer rollback, pero lanzamos la excepción antes de insertar el segundo dato así que el flujo del programa no llegaría al código de insertarlo, pero si hace el primero, ergo solo insertamos un dato. Podemos imaginar que hay algo así:
public void hazAlgo(Entidad entidad) {
try{
tx.begin
// inserto un dato en bbdd
throw new FileNotFoundException();
// inserto otro dato en bbdd
}
catch(Exception e ) {
if( e instanceof RuntimeException ) {
tx.rollback();
tx.close();
}
else {
tx.commit();
tx.close();
}
throw e;
}
}
¡Esto no es el código que hay realmente! Tan solo es poner en pseudocódigo lo explicado anteriormente.
Compliquémoslo un poco más:
public class A {
@Autowired
private B b;
public void hazAlgoA() {
b.hazAlgoB();
}
}
class B {
@Autowired
private C c;
@Transactional
public void hazAlgoB() {
// inserto un dato en bbdd
try {
c.hazAlgoC();
catch( NullPointerExecption e) {
}
// inserto otro dato en bbdd
}
}
class C {
@Transactional
public void hazAlgoC() {
// inserto un dato en bbdd
throw new NullPointerException();
// inserto otro dato en bbdd
}
}
Esta es más interesante. La clase C lanza una NullPointerException que hereda una RuntimeException ergo debería hacer un rollback. Pero, pero, pero, la clase B, donde se inicia la transacción, ya tenía previsto este error, así que captura la excepción y no la vuelve a lanzar. ¿Hará un rollback? ¿Hará un commit? Hagan sus apuestas.
Bueno, hace un rollback. El punto es que si una RuntimeExcepcition sale de un método anotado como @Transactional, toda la transacción queda marcada como rollback. No importa que no fuese el método desde el que se comenzó la transacción, si su propagación es normal (esto es, no es una REQUIRES_NEW), en cuanto lanzamos la excepción estamos vendidos.
Esto es importante por un motivo, hay gente que, ante la duda, anota todos los métodos o las clases con @Transactional; sin embargo, esto supone un problema de concepto de construcción. El encargado de saber si la transacción ha de marcarse como rollback o commit es siempre el método desde donde se inicia la transacción, nunca uno de los métodos a los que llama. Si yo llamo a un método externo que está marcado como transaccional y este por algún motivo me lanza una RuntimeExcepción, automáticamente me obliga a hacer un rollback. No me deja la opción de que sea yo el que valore si puedo continuar o no, quizás ya tenía previsto que podía saltar esa excepción y la capturaba para tratarla. Lamentablemente, al utilizar una librería que anotó su método con @Transactional me limita de cualquier intento de recuperarme de la excepción.
Moraleja, solo hay que poner @Transactional en métodos donde se abre, cierra y controla la transacción. En el resto de métodos su colocación puede ser contraproducente.
Controlar las excepciones que hagan commit o rollback
El hecho anterior, que una transacción haga rollback si se lanza una RuntimeExcecption y commit si no, es algo demasiado rígido para muchos casos. Hay momentos que no queremos que se haga un rollback para alguna excepción en concreto o lo contrario, no queremos que haga un commit cuando se lanza una excepción que no herede de RuntimeException. Para esto tenemos las propiedades noRollbackFor y rollbackFor, con estas opciones podemos configurar el commit y el rollback según nuestras necesidades.
Por ejemplo:
@Transactional(noRollbackFor={NumberFormatException.class,ArithmeticException.class})
public void hazAlgoTransaccionalmente() {
// Soy transaccional!
}
Hará un commit incluso si lanza las excepciones NumberFormatException o ArithmeticException o excepciones que hereden de estas. Lo de las excepciones que hereden de esta es muy importante para tener en cuenta a su alcance. Hay gente que lo aprovecha para hacer cosas del tipo:
@Transactional(noRollbackFor={RuntimeException.class})
public void hazAlgoTransaccionalmente() {
// Soy transaccional!
}
Lo cual es una muy pésima idea porque implica que el método nunca hará un rollback.
Tal como he dicho existe la propiedad inversa, rollbackFor. Con esta podremos conseguir que la transacción haga un rollback para las excepciones que no hereden de RuntimeException.
@Transactional(rollbackFor={FileNotFoundException.class})
public void hazAlgoTransaccionalmente() {
// Soy transaccional!
}
Hará un rollback si lanza una FileNotFoundException a pesar de que esta no hereda de RuntimeException.
Transacciones programáticas
No siempre se pueden abrir transacciones con @Transactional. A veces necesitamos crear una transacción en algún lugar que la anotación no nos permite (un método privado, por ejemplo) así que hemos de buscarnos un método alternativo. Para esto utilizaremos las transacciones programáticas Con estas podremos abrir y cerrar transacciones sin necesidad de anotaciones
Veamos el siguiente código:
Obejct o = new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
// Soy Transaccional!
}
});
Con él podemos abrir la transacción sin necesidad de anotaciones.
El código se puede modificar para añadir diferentes tipos de transacciones (propagación o aislamiento), para que nos devuelva un resultado ((TransactionCallback) o no (TransactionCallbackWithoutResult), etc. Para más información leer el manual
Lo que me dejo en el tintero
Aislamiento de Transacciones (Isolation)
El nivel de aislamiento de una transacción define cómo las operaciones realizadas dentro de una transacción son visibles para otras transacciones y cómo se manejan las operaciones concurrentes. Cambiar el nivel de aislamiento puede tener implicaciones significativas, permitiendo, por ejemplo, que una consulta devuelva datos que han sido insertados pero aún no se han confirmado (commit). Los niveles de aislamiento (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE) ofrecen un equilibrio entre rendimiento y consistencia de datos, afectando a fenómenos como las lecturas fantasma, las condiciones de carrera, y otros problemas de concurrencia.
Duración de la Transacción
Las transacciones muy largas, que involucran cientos o miles de inserciones o modificaciones, pueden ser problemáticas debido al consumo de memoria y a la complejidad en la gestión de la consistencia de datos a lo largo del tiempo. En general, es preferible mantener transacciones pequeñas y bien definidas, que limitan el alcance de los cambios y reducen la probabilidad de conflictos y errores.
Transacciones y Aspectos
La programación orientada a aspectos (AOP) permite superar algunas de las limitaciones de las anotaciones en Spring, como la incapacidad de aplicar @Transactional
a métodos privados o la necesidad de anotar explícitamente el método de entrada de la clase. A través de bibliotecas como AspectJ, es posible interceptar llamadas a métodos y aplicar manejo transaccional de forma más flexible y potente, aunque este enfoque requiere una comprensión sólida de AOP y puede aumentar la complejidad del diseño.
Transacciones Declarativas en Configuración
Spring también ofrece la posibilidad de definir transacciones de manera declarativa en los archivos de configuración. Esto permite especificar políticas transaccionales para métodos o clases enteras basándose en patrones de nombres u otros criterios, como hacer que todos los métodos cuyo nombre comienza por «get» sean de solo lectura, y aquellos que comienzan por «insert» inicien transacciones normales. Aunque potente, este enfoque puede resultar demasiado genérico para ser útil en todos los casos y no es comúnmente usado en muchos proyectos. No obstante, es valioso conocer esta capacidad por las posibilidades que ofrece en términos de diseño y configuración de transacciones.
Una pregunta sobre readonly. El comportamiento que explicas es referente al atributo readonly de la anotación @Transactional, pero no se refiere a la propiedad mutable de una entidad que por ejemplo este mapeada con un fichero hbm.xml. ¿No? Me refiero a que si tu en tu fichero de entidad hbm.xml seteas el atributo mutable=false no hace updates sobre la base de datos, independientemente de la base de datos, no? Gracias. Saludos.
Hola, es tal como dices. El readonly de la transacción no tiene nada que ver con el mutable=false del mapeo de la clase. El mutable no te actualizará la entidad nunca, mientras que el readonly simplemente «optimizará» la transacción para que vaya más rápido. El ejemplo ideal de mutable=false sería para mapear una tabla maestra de países, por ejemplo, entidades que sabes que no se modificarán nunca; mientras que el readonly podría ser para una transacción que hace un informe, hay entidades que generalmente se modifican pero en ese caso concreto sabes que sólo vas a utilizarlas en solo lectura. Saludos
Hola, estoy desarrollando un sistema con spring mvc (Controller, Service y DAO). Ahora bien mis daos heredan un GenericDao que utiliza la plantilla Hibernate (HibernateTemplate). Resulta que desde mi servicio llamo a varios daos (insert update’s). Ahora bien en mi servicio e colocado un @transactional y en mis los metodos de mis daos un @transacional(support). Cuando corro la aplicación me aparece la excepción: UnexpectedRollbackException: : Transaction rolled back because it has been marked as rollback-only. A la espera de tus comentarios, gracias
Hola. No tiene mucho misterio. Si la transacción ha quedado marcada como rollback es que te ha saltado una excepción que hereda de RuntimeException. Has de averiguar qué excepción es y porqué sale. Saludos
Muchisimas gracias por este tutorial, me ha servido para entender muchas cosas! Ahora lo pondré en practica 🙂
En mi caso estoy trabajando con Mybatis también, tengo un metodo en el controller que llama a varios servicios, cada uno de ellos borrar o inserta en BBDD, este método lo tengo con el @Transactional y controlando el rollbackFor = Exception.class para que haga el rollback en cualquier excepcion… pero no lo hace… veo que por cada insert o delete se habre una sqlSession, la cual hace commit segun termina… por lo tanto si mas adelante cualquiera de los deletes o los inserts falla, el commit ya esta hecho de las operaciones anteriores…
Que es lo que pasa?? no funciona el Transactional, no veo ninguna traza que indique nada acerca del rollback 🙁
Hola, el problema de la anotación @Transactional es que si hay algo mal configurado no te avisa, simplemente la ignora. Comprueba que la configuración es la correcta.
Segundo, lo más evidente. Si dentro de tu controller haces catch de la eception y no la dejas salir del método, nunca te haría un rollback. Comprueba que la excepción sale, porque por lo que me comentas no parece que lo esté haciendo.
Si no es nada de todo esto, la verdad es que desconozco como funciona Mybatis. En Hibernate, si no hay una transacción abierta al hacer un insert te salta una excepción lo cual te deja ver que no has configurado bien Spring. Pero si haces una consulta y no hay transacción abierta, hibernate crea una transacción para la consulta y luego la cierra él mismo. El comportamiento que me comentas es parecido. ¿Quizás MyBatis abre el mismo la transacción al no encontrar ninguna? Siento no poder ayudarte más.
Saludos
Creo que los tiros pueden ir por aqui… http://stackoverflow.com/questions/8176465/mybatis-spring-setup-not-using-transactions
Pero no según dice como lo resolvió, haciendo un «spring-managed bean» el método anotado con @Transactional… como se hace eso??
Por otra parte, voy a intentar explicar mejor como tengo montado el tinglao. De modo bastante resumido.
Tengo un controller, con los siguientes metodos:
————————————————————————————————————–
public void metodo1() {
metodo2()
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public metodo2() throws IllegalStateException, IOException, SQLException, Exception {
borrarDatos();
insertarInsertarDatos();
}
public borrarDatos() throws IllegalStateException, IOException, SQLException, Exception{
try {
servicio.borraDatos();
} catch (Exception e){
System.out.println(«error al borrar datos»):
throw e;
}
}
public insertarDatos() throws IllegalStateException, IOException, SQLException, Exception{
try {
servicio.insertaDatos();
} catch (Exception e){
System.out.println(«error al insertar datos»):
throw e;
}
}
————————————————————————————————————-
En mi caso, tengo los datos preparados para que al insertar, salte sqlException, por lo tanto deberia hacer un rollback de los datos que ha borrado, pero no lo hace, se borran los datos y no se inserta ninguno….
Gracias!
Y este es mi fichero de configuracion
Hola. Estas llamando a método2 desde método1. La anotación @Trandactional ha de estar siempre en el método de entrada a la clase, en tu caso método1. Piensa que las clases de Spring son proxies. Esta explicado casi al principio del tutorial.
Saludos
Hola, esta genial el tutorial, me gustaría saber si no tienes un ejemplo completo porque en mi caso no se como deberían estar los DAO’s y donde poner la configuración de Spring para que acepte las transacciones.
Saludos
Hola, hay un tutorial simple de eso al principio del blog: http://igochan.wordpress.com/category/tutorial/tutorial-spring/
Saludos