Manejo transaccional de la base de datos con Spring Framework y AOP

Introducción

Manejar la transaccionabilidad contra una base de datos no es un tema complicado al utilizar JDBC, menos aún utilizando un bean TransactionTemplate de Spring. Sin embargo, siempre que hacemos tareas repetitivas -como escribir código para empezar una trasacción, hacer un commit en caso de éxito o hacer rollback en caso de error- abre la puerta a olvidos o errores de programación.

Spring pone a disposición buenas herramientas para el manejo transaccional contra una base de datos, ya sea de forma programática o de forma declarativa. Si elegimos el enfoque programático, contamos con la clase org.springframework.transaction.support.TransactionTemplate. Si preferimos manejar nuestras transacciones de forma declarativa, contamos con opciones de configuración mediante XMLs y AOP (tx:advice) o utilizando annotations (org.springframework.transaction.annotation.Transactional)
Veremos los diferentes atributos en la definición de una transacción, y las formas de uso de cada uno de los enfoques.

Atributos de una transacción

name

Nombre de la transacción. Por defecto no tienen nombre, y si tienen, este nombre se muestra en los monitores transaccionales, cuando el concepto es aplicable.

propagation

Es el comportamiento de la propagación de la transacción. Por propagación entendemos la forma de asociar la transacción al método que estamos ejecutando. Entre los posibles comportamientos tenemos:

  • REQUIRED: Comportamiento por defecto. Si existe una transacción en curso, el método corre dentro de esa transacción. Si no existe una en curso, creará una nueva transacción.
    tx_prop_required
  • REQUIRES_NEW: Indica que el método debe
    correr dentro de su propia transacción. Si existe una transacción en curso, se suspenderá durante la duración de la invocación al método.
    tx_prop_requires_new
  • MANDATORY: Indica que el método debe correr dentro de una transacción. Si no existe una transacción en curso, se lanzará una excepción.
  • NESTED: Indica que si existe una transacción en progreso, el método se ejecutará dentro de una transacción anidada. Sobre esta transacción se ejecutará commit o rollback sin afectar las otras, (utilizando savepoints) . Si no existe ninguna transacción en curso, se comporta de la misma forma que REQUIRED.
  • NEVER: Indica que el método no debe ser ejecutado utilizando una transacción en curso. Si existe una transacción en curso, se lanzará una
    excepción.
  • NOT_SUPPORTED: Indica que el método no debe ser ejecutado dentro de una transacción. Si existe una en progreso, se suspenderá durante la ejecución del método.
  • SUPPORTS: Indica que el método no requiere una transacción, pero se utilizará si existese alguna en curso.

isolation

El isolation level de la transacción. Por isolation level entendemos la configuración de qué tanto afectan los cambios del DBMS que son externos a nuestra transacción. En el nivel de aislación más bajo vemos cómo van cambiando los datos desde que comenzó nuestra transacción, inclusive si son resultados intermedios de otra transacción. En el nivel más alto, trabajamos con una foto de los datos que no cambia mientras nuestra transacción vive. Entre los posibles valores tenemos:

  • DEFAULT: Utiliza el nivel por defecto, lo determinará la fuente de datos.
  • READ_UNCOMMITED: Este nivel permite que en una transacción se puedan leer datos que aún no han sido commiteados en otras transacciones (lecturas sucias). Pueden suceder además lecturas fantasma (si se hace rollback de la otra transacción, los datos que leímos no fueron válidos) y lecturas no repetibles (una misma consulta puede retornar valores diferentes dentro de la misma transacción).
  • READ_COMMITED: Este nivel no expone datos de una tupla aún no commiteados por otras transacciones, pero permite que otras transacciones cambien datos de las tuplas recuperadas por nuestra transacción. De esta forma, las lecturas sucias se evitan, pero pueden suceder las lecturas fantasma y las lecturas no repetibles.
  • REPEATABLE_READ: Este nivel no permite leer datos aún no commiteados de una tupla, y tampoco permite modificar los datos leídos por una transacción en curso. Sin embargo, si hacemos 2 consultas (SELECTs) con una condición de rango en el WHERE, y otra transacción inserta tuplas entre las 2 consultas y que cumplan con la condición de rango, las consultas retornarán conjuntos de tuplas diferentes. Las lecturas sucias y las lecturas no repetibles se evitan, pero pueden suceder las lecturas fantasma.
  • SERIALIZABLE: Además de las condiciones de REPEATABLE_READ, evita el caso en que en nuestra transacción hacemos una consulta con una condición de rango, otra transacción inserta una tupla que cae cumple la condición, y luego repetimos la consulta en nuestra transacción. Si el es isolation level es SERIALIZABLE, ambas consultas retornarán el mismo resultado. Se evitan las lecturas sucias, las lecturas no repetibles y las lecturas fantasma.

timeout

El timeout de la transacción, expresado en segundos.

read-only

Indica si la transacción debe ser tratada como solo lectura, posibilitando así optimizaciones adicionales.

Uso de transacciones con Spring

Definición de forma programática

Para manejar la transaccionabilidad de forma programática, debemos
tener una referencia a org.springframework.transaction.support.TransactionTemplate en nuestra clase. Para ello, definiremos algunas relaciones entre nuestros beans:

<br />
&lt;bean name=&quot;hibernateTemplate&quot; class=&quot;org.springframework.orm.hibernate3.HibernateTemplate&quot;&gt;<br />
	&lt;constructor-arg ref=&quot;sessionFactory&quot;/&gt;<br />
&lt;/bean&gt;</p>
<p>&lt;bean name=&quot;transactionManager&quot; class=&quot;org.springframework.orm.hibernate3.HibernateTransactionManager&quot;&gt;<br />
	&lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot; /&gt;<br />
	&lt;property name=&quot;sessionFactory&quot; ref=&quot;sessionFactory&quot; /&gt;<br />
&lt;/bean&gt;</p>
<p>&lt;bean name=&quot;transactionTemplate&quot; class=&quot;org.springframework.transaction.support.TransactionTemplate&quot;&gt;<br />
	&lt;constructor-arg ref=&quot;transactionManager&quot;/&gt;<br />
&lt;/bean&gt;<br />

En este ejemplo el framework de ORM utilizado es Hibernate, pero la idea aplica para cualquier framework ORM de los que Spring tiene soporte.
El bean hibernateTemplate entonces es el que agrupa funcionalidades de acceso a datos, apoyándose en las funcionalidades de Hibernate.
El bean transactionManager es una subclase de org.springframework.transaction.support.AbstractPlatformTransactionManager, implementando el manejo de transacciones (determinación de transacciones existentes, suspensión de transacciones, definición del comportamiento transaccional, etc.).
Por último, transactionTemplate simplifica el manejo transaccional y el manejo de excepciones por errores.

La forma de uso del transactionTemplate dentro de nuesta aplicación es la siguiente:

<br />
public class MyEntityDAOImpl implements MyEntityDAO {</p>
<p>	private HibernateTemplate hibernateTemplate;</p>
<p>	private TransactionTemplate transactionTemplate;	</p>
<p>	public List&lt;MyEntity&gt; getAll() {<br />
		return (List&lt;MyEntity&gt;)<br />
getTransactionTemplate().execute(new TransactionCallback() {</p>
<p>			@Override<br />
			public Object doInTransaction(TransactionStatus status) {<br />
				return getHibernateTemplate().loadAll(MyEntity.class);<br />
			}<br />
		});<br />
	}</p>
<p>	/*<br />
	...<br />
	setters y getters de hibernateTemplate y transactionTemplate<br />
	...<br />
	*/<br />
}<br />

Todo lo que pase dentro del método doInTransaction estará encapsulado dentro de una transacción.

Definición de forma declarativa – XMLs

El bean nos permite crear un aspecto (por más info acerca de AOP, ver este post) encargado de iniciar una transacción contra la base de datos al invocar un método que encaje en la definición del pointcut. En este pointcut se podría definir para un método de una clase, para toda la clase, o incluso para todo un paquete que identifique la capa de negocios o de acceso a datos de nuestra aplicación.

Para configurar un en nuestra aplicación, incluiremos en nuestros XMLs de Spring los siguientes beans:

<br />
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br />
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;<br />
	xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;<br />
	xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;<br />
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd<br />
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd<br />
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;</p>
<p>	&lt;aop:config&gt;<br />
		&lt;aop:advisor pointcut=&quot;execution(public * demo.dao.*.create(..))&quot; advice-ref=&quot;tx-advice&quot; /&gt;<br />
	&lt;/aop:config&gt;</p>
<p>	&lt;tx:advice id=&quot;tx-advice&quot;&gt;<br />
		&lt;tx:attributes&gt;<br />
			&lt;tx:method name=&quot;*&quot; propagation=&quot;REQUIRED&quot; isolation=&quot;DEFAULT&quot; rollback-for=&quot;Throwable&quot; no-rollback-for=&quot;BusinessException&quot; /&gt;<br />
			&lt;tx:method name=&quot;get*&quot; propagation=&quot;SUPPORTS&quot; read-only=&quot;true&quot; /&gt;<br />
		&lt;/tx:attributes&gt;<br />
	&lt;/tx:advice&gt;</p>
<p>&lt;/beans&gt;<br />

Tendiendo hacia convención en lugar de configuración, asume que existe un bean llamado «transactionManager», instancia de la clase -en caso de Hibernate- org.springframework.orm.hibernate3.HibernateTransactionManager. Si nuestro transaction manager tiene un nombre diferente, debemos referenciarlo incluyendo un atributo transaction-manager al bean .

El atributo name es el único parámetro requerido, y tiene un significado diferente al que se le da en la configuración programática. En este caso, sirve para filtrar los nombres de los métodos que se asociarán a esta las transacciones. Soporta comodines (*) en los nombres, por ejemplo ‘get*’, ‘handle*’, ‘on*Event’.

Al bean le podemos configurar todos los atributos descritos al inicio del post (propagation, isolation leven, timeout, etc.), y además:

  • rollback-for: Lista de excepciones -separadas por comas- para las cuales se dispara un rollback de la transacción
  • no-rollback-for: Lista de excepciones -separadas por comas- para las cuales no se dispara un rollback de la transacción

Uso de forma declarativa – @Annotations

Si bien simplifica mucho el manejo de transacciones, podemos utilizar annotations para simplificarlo aún más.

Utilizando sólo una línea en la configuración XML de nuestra aplicación podemos dar lugar a configurar el manejo transaccional a nivel de annotations. Esta línea tiene el siguiente formato:

<br />
&lt;tx:annotation-driven transaction-manager=&quot;txManager&quot; /&gt;<br />

El atributo transaction-manager debe ser incluido si nuestro transaction manager no se llama «transactionManager».

El tener configurado este bean nos habilita a poder anotar nuestras clases y métodos con @Transactional y definir de esta forma nuestras transacciones de forma declarativa y dentro de nuestro código Java.

<br />
@Transactional(propagation=Propagation.SUPPORTS, readOnly=true)<br />
public class MyServiceImpl implements MyService {</p>
<p>	...</p>
<p>	@Transactional(propagation=Propagation.REQUIRED, readOnly=false)<br />
	public void addMyEntity(MyEntity e) {<br />
		...<br />
	}</p>
<p>	...<br />
}<br />

En este ejemplo, todos los métodos de la clase son anotados como de solo lectura y con el comportamiento de propagación SUPPORTS. Al método addMyEntity se le especifica un comportamiento diferente, anotándolo con el comportamiento de propagación REQUIRED, y eliminando la restricción de solo lectura.

Cabe destacar que @Transactional puede ser utilizado también en interfaces indicando el comportamiento de todas las implementaciones de dicha interface.

Conclusión

Un manejo transaccional apropiado en nuestras aplicaciones es cada día más importante para asegurar robustez, especialmente en ambientes inciertos (desconexiones, latencias) y de alta concurrencia.

Spring nos brinda posiblidades concretas y muy poderosas para aislarnos de la problemática del manejo transaccional, dándonos opciones para las diferentes preferencias de enfoque y opciones en el desarrollo de aplicaciones. Vimos desde cómo hacer para declarar transacciones en el código, teniendo total control acerca del inicio y fin de las mismas. También vimos cómo declarar el ciclo de vida de las transacciones utilizando AOP y Spring juntos, a través de configuración XML. Finalmente vimos aún más simplificada la tarea, habilitando la configuración transaccional mediante annotations, y de esta manera definir en nuestras clases -o interfaces- el comportamiento deseado.

Hoy en día podemos hacer uso de esta flexibilidad y potencia, que quizá hasta hace poco estaba restringida a EJBs, utilizando un framework no intrusivo como Spring, un concepto cada ves más utilizado como AOP y nuestros viejos y queridos POJOs para nuestra lógica.

Referencias

Etiquetado , , . Bookmark the permalink.

6 respuestas a Manejo transaccional de la base de datos con Spring Framework y AOP

  1. Dante says:

    muy bueno…

  2. Jose Velasco says:

    Una preguntilla si eres tan amable…
    Si dentro de un metodo anotado con isolation= Isolation.READ_COMMITED se lee un registro que está siendo modificado por otra transacción pero aún no se ha hecho commit, ¿Qué ocurre al leerlo? Se lanza una excepción…

    Muchas gracias

    • jarrarte says:

      José,

      Si el isolation level de la transacción en la que estás es READ_COMMITED, lo que te va a pasar es que cuando leas un registro que está siendo modificado vas a recuperar el valor «viejo» hasta que se haga commit de la actualización del registro. No da error, pero lo que te puede pasar es que tengas «phantom reads»: ejecutas la misma consulta en 2 instantes en el tiempo y tienes resultados diferentes (porque entre las consultas hubo un commit a los registros afectados)

      De la API de Isolation:

      A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur. This level only prohibits a transaction from reading a row with uncommitted changes in it.

      Espero haberte sido de utilidad.
      Saludos,
      José

  3. Frans says:

    Gracias Jose. Me sirvio de mucho

  4. Santiago says:

    Excelente , me sirvio mucho para comprender aun mas la anotacion @Transactional!
    Gracias

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *