Ejemplo de Gestión de Transacciones de Primavera con JDBC

La gestión de transacciones de Spring es una de las características más ampliamente utilizadas e importantes del framework Spring. La gestión de transacciones es una tarea trivial en cualquier aplicación empresarial. Ya hemos aprendido cómo usar la API de JDBC para la gestión de transacciones. Spring proporciona un amplio soporte para la gestión de transacciones y ayuda a los desarrolladores a enfocarse más en la lógica del negocio en lugar de preocuparse por la integridad de los datos en caso de fallos del sistema.

Gestión de transacciones de Spring

Algunos de los beneficios de usar la Gestión de Transacciones de Spring son:

  1. Soporte para la Gestión de Transacciones Declarativa. En este modelo, Spring utiliza AOP sobre los métodos transaccionales para proporcionar integridad de datos. Este es el enfoque preferido y funciona en la mayoría de los casos.
  2. Soporte para la mayoría de las APIs de transacción como JDBC, Hibernate, JPA, JDO, JTA, etc. Todo lo que necesitamos hacer es usar la clase de implementación adecuada del administrador de transacciones. Por ejemplo, org.springframework.jdbc.datasource.DriverManagerDataSource para el manejo de transacciones JDBC y org.springframework.orm.hibernate3.HibernateTransactionManager si estamos usando Hibernate como herramienta ORM.
  3. Soporte para el manejo de transacciones programático mediante el uso de la implementación de TransactionTemplate o PlatformTransactionManager.

La mayoría de las características que querríamos en un administrador de transacciones son compatibles con el manejo de transacciones declarativo, por lo que usaríamos este enfoque para nuestro proyecto de ejemplo.

Ejemplo de Gestión de Transacciones de Spring con JDBC

Vamos a crear un proyecto simple de Spring JDBC donde actualizaremos varias tablas en una única transacción. La transacción debería confirmarse solo cuando todas las declaraciones JDBC se ejecuten correctamente; de lo contrario, debería revertirse para evitar inconsistencias en los datos. Si estás familiarizado con la gestión de transacciones JDBC, podrías argumentar que podemos hacerlo fácilmente configurando auto-commit en falso para la conexión y, según el resultado de todas las declaraciones, confirmar o revertir la transacción. Obviamente, podemos hacerlo de esa manera, pero eso resultaría en mucho código redundante solo para la gestión de transacciones. Además, el mismo código estaría presente en todos los lugares donde buscamos la gestión de transacciones, lo que causaría un código fuertemente acoplado y difícil de mantener. La gestión de transacciones declarativa de Spring aborda estas preocupaciones utilizando la Programación Orientada a Aspectos para lograr un acoplamiento flexible y evitar código redundante en nuestra aplicación. Veamos cómo lo hace Spring con un ejemplo sencillo. Antes de sumergirnos en nuestro proyecto de Spring, realicemos algunas configuraciones de base de datos para nuestro uso.

Spring Transaction Management – Configuración de la Base de Datos

Crearemos dos tablas para nuestro uso y las actualizaremos ambas en una única transacción.

CREATE TABLE `Customer` (
  `id` int(11) unsigned NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Address` (
  `id` int(11) unsigned NOT NULL,
  `address` varchar(20) DEFAULT NULL,
  `country` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Podríamos definir la relación de clave externa aquí desde la columna id de Dirección a la columna id de Cliente, pero por simplicidad no tengo ninguna restricción definida aquí. Nuestra configuración de base de datos está lista para el proyecto de gestión de transacciones de primavera, creemos un proyecto Maven de Spring simple en Spring Tool Suite. La estructura final de nuestro proyecto se verá como en la siguiente imagen. Veamos cada una de las piezas una por una, juntas proporcionarán un ejemplo simple de gestión de transacciones de primavera con JDBC.

Gestión de Transacciones de Spring – Dependencias Maven

Dado que estamos utilizando la API de JDBC, tendríamos que incluir la dependencia spring-jdbc en nuestra aplicación. También necesitaríamos el controlador de base de datos MySQL para conectarnos a la base de datos MySQL, así que también incluiremos la dependencia mysql-connector-java. El artefacto spring-tx proporciona dependencias de gestión de transacciones, por lo general se incluye automáticamente por STS, pero si no es así, entonces también necesitas incluirlo. Es posible que veas algunas otras dependencias para el registro y las pruebas unitarias, sin embargo, no vamos a usar ninguna de ellas. Nuestro archivo pom.xml final se ve como el siguiente código.

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.springframework.samples</groupId>
	<artifactId>SpringJDBCTransactionManagement</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>

		<!-- Generic properties -->
		<java.version>1.7</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

		<!-- Spring -->
		<spring-framework.version>4.0.2.RELEASE</spring-framework.version>

		<!-- Logging -->
		<logback.version>1.0.13</logback.version>
		<slf4j.version>1.7.5</slf4j.version>

		<!-- Test -->
		<junit.version>4.11</junit.version>

	</properties>

	<dependencies>
		<!-- Spring and Transactions -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>

		<!-- Spring JDBC and MySQL Driver -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.0.5</version>
		</dependency>

		<!-- Logging with SLF4J & LogBack -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>${logback.version}</version>
			<scope>runtime</scope>
		</dependency>

		<!-- Test Artifacts -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring-framework.version}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>
</project>

I have updated the Spring versions to the latest one as of today. Make sure MySQL database driver is compatible with your mysql installation.

Gestión de Transacciones de Spring – Clases del Modelo

Crearemos dos Java Beans, Cliente y Dirección, que se mapearán con nuestras tablas.

package com.journaldev.spring.jdbc.model;

public class Address {

	private int id;
	private String address;
	private String country;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
	public String getCountry() {
		return country;
	}
	public void setCountry(String country) {
		this.country = country;
	}
	
}
package com.journaldev.spring.jdbc.model;

public class Customer {

	private int id;
	private String name;
	private Address address;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Address getAddress() {
		return address;
	}
	public void setAddress(Address address) {
		this.address = address;
	}
	
}

Observa que el bean Cliente tiene Dirección como una de sus variables. Cuando implementemos el DAO para Cliente, obtendremos datos tanto de la tabla de clientes como de la de direcciones, y ejecutaremos dos consultas de inserción separadas para estas tablas, por eso necesitamos la gestión de transacciones para evitar inconsistencias en los datos.

Gestión de Transacciones de Spring – Implementación de DAO

Implementemos el DAO para el bean Cliente, para simplicidad solo tendremos un método para insertar registros en ambas tablas, cliente y dirección.

package com.journaldev.spring.jdbc.dao;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerDAO {

	public void create(Customer customer);
}
package com.journaldev.spring.jdbc.dao;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import com.journaldev.spring.jdbc.model.Customer;

public class CustomerDAOImpl implements CustomerDAO {

	private DataSource dataSource;

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	@Override
	public void create(Customer customer) {
		String queryCustomer = "insert into Customer (id, name) values (?,?)";
		String queryAddress = "insert into Address (id, address,country) values (?,?,?)";

		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

		jdbcTemplate.update(queryCustomer, new Object[] { customer.getId(),
				customer.getName() });
		System.out.println("Inserted into Customer Table Successfully");
		jdbcTemplate.update(queryAddress, new Object[] { customer.getId(),
				customer.getAddress().getAddress(),
				customer.getAddress().getCountry() });
		System.out.println("Inserted into Address Table Successfully");
	}

}

Observa que la implementación del CustomerDAO no se encarga de la gestión de transacciones. De esta manera estamos logrando la separación de preocupaciones porque a veces obtenemos implementaciones de DAO de terceros y no tenemos control sobre estas clases.

Administración Declarativa de Transacciones de Spring – Servicio

Creemos un Servicio de Cliente que utilizará la implementación de CustomerDAO y proporcionará gestión de transacciones al insertar registros en las tablas de cliente y dirección en un solo método.

package com.journaldev.spring.jdbc.service;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerManager {

	public void createCustomer(Customer cust);
}
package com.journaldev.spring.jdbc.service;

import org.springframework.transaction.annotation.Transactional;

import com.journaldev.spring.jdbc.dao.CustomerDAO;
import com.journaldev.spring.jdbc.model.Customer;

public class CustomerManagerImpl implements CustomerManager {

	private CustomerDAO customerDAO;

	public void setCustomerDAO(CustomerDAO customerDAO) {
		this.customerDAO = customerDAO;
	}

	@Override
	@Transactional
	public void createCustomer(Customer cust) {
		customerDAO.create(cust);
	}

}

Si observas la implementación de CustomerManager, simplemente utiliza la implementación de CustomerDAO para crear al cliente, pero proporciona gestión de transacciones declarativa al anotar el método createCustomer() con la anotación @Transactional. Eso es todo lo que necesitamos hacer en nuestro código para obtener los beneficios de la gestión de transacciones de Spring. La anotación @Transactional se puede aplicar tanto a métodos como a toda la clase. Si deseas que todos tus métodos tengan funciones de gestión de transacciones, debes anotar tu clase con esta anotación. Lee más sobre anotaciones en Tutorial de Anotaciones en Java. La única parte que queda es conectar los beans de Spring para que funcione el ejemplo de gestión de transacciones de Spring.

Administración de Transacciones de Spring – Configuración de Beans

Cree un archivo de configuración de Spring Bean con el nombre “spring.xml”. Lo usaremos en nuestro programa de prueba para conectar los beans de Spring y ejecutar nuestro programa JDBC para probar la gestión de transacciones.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
	xmlns:tx="https://www.springframework.org/schema/tx"
	xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.0.xsd
		https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

	<!-- Enable Annotation based Declarative Transaction Management -->
	<tx:annotation-driven proxy-target-class="true"
		transaction-manager="transactionManager" />

	<!-- Creating TransactionManager Bean, since JDBC we are creating of type 
		DataSourceTransactionManager -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- MySQL DB DataSource -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">

		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
		<property name="username" value="pankaj" />
		<property name="password" value="pankaj123" />
	</bean>

	<bean id="customerDAO" class="com.journaldev.spring.jdbc.dao.CustomerDAOImpl">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<bean id="customerManager" class="com.journaldev.spring.jdbc.service.CustomerManagerImpl">
		<property name="customerDAO" ref="customerDAO"></property>
	</bean>

</beans>

Los puntos importantes a tener en cuenta en el archivo de configuración de beans de Spring son:

  • tx:annotation-driven se utiliza para indicar al contexto de Spring que estamos utilizando una configuración de gestión de transacciones basada en anotaciones. El atributo transaction-manager se utiliza para proporcionar el nombre del bean del administrador de transacciones. El valor predeterminado de transaction-manager es transactionManager, pero aún lo estoy usando para evitar confusiones. El atributo proxy-target-class se utiliza para indicar al contexto de Spring que utilice proxies basados en clases; sin él, obtendrá una excepción en tiempo de ejecución con un mensaje como Excepción en el hilo “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: El bean llamado ‘customerManager’ debe ser del tipo [com.journaldev.spring.jdbc.service.CustomerManagerImpl], pero en realidad era del tipo [com.sun.proxy.$Proxy6]
  • Dado que estamos utilizando JDBC, estamos creando el bean transactionManager de tipo org.springframework.jdbc.datasource.DataSourceTransactionManager. Esto es muy importante y debemos utilizar la clase de implementación adecuada del administrador de transacciones en función del uso de nuestra API de transacciones.
  • El bean dataSource se utiliza para crear el objeto DataSource y debemos proporcionar las propiedades de configuración de la base de datos, como driverClassName, url, nombre de usuario y contraseña. Cambie estos valores según la configuración local.
  • Estamos inyectando dataSource en el bean customerDAO. De manera similar, estamos inyectando el bean customerDAO en la definición del bean customerManager.

Nuestra configuración está lista, creemos una clase de prueba simple para probar nuestra implementación de gestión de transacciones.

package com.journaldev.spring.jdbc.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.journaldev.spring.jdbc.model.Address;
import com.journaldev.spring.jdbc.model.Customer;
import com.journaldev.spring.jdbc.service.CustomerManager;
import com.journaldev.spring.jdbc.service.CustomerManagerImpl;

public class TransactionManagerMain {

	public static void main(String[] args) {
		ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
				"spring.xml");

		CustomerManager customerManager = ctx.getBean("customerManager",
				CustomerManagerImpl.class);

		Customer cust = createDummyCustomer();
		customerManager.createCustomer(cust);

		ctx.close();
	}

	private static Customer createDummyCustomer() {
		Customer customer = new Customer();
		customer.setId(2);
		customer.setName("Pankaj");
		Address address = new Address();
		address.setId(2);
		address.setCountry("India");
		// estableciendo un valor de más de 20 caracteres, para que ocurra una SQLException
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Nota que estoy estableciendo explícitamente el valor de la columna de dirección demasiado largo para que obtengamos una excepción al insertar datos en la tabla de direcciones. Ahora, cuando ejecutamos nuestro programa de prueba, obtenemos la siguiente salida.

Mar 29, 2014 7:59:32 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3fa99295: startup date [Sat Mar 29 19:59:32 PDT 2014]; root of context hierarchy
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Inserted into Customer Table Successfully
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.support.SQLErrorCodesFactory <init>
INFO: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into Address (id, address,country) values (?,?,?)]; Data truncation: Data too long for column 'address' at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
	at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:100)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:907)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:968)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:978)
	at com.journaldev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$$84f71441.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
	at com.journaldev.spring.jdbc.main.TransactionManagerMain.main(TransactionManagerMain.java:20)
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
	at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
	at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
	at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:914)
	at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:907)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642)
	... 16 more

Ten en cuenta que el mensaje de registro indica que los datos se insertaron correctamente en la tabla de clientes, pero la excepción lanzada por el controlador de base de datos MySQL claramente indica que el valor es demasiado largo para la columna de dirección. Ahora, si revisas la tabla de Clientes, no encontrarás ninguna fila allí, lo que significa que la transacción se ha revertido por completo. Si te preguntas dónde está ocurriendo la magia del manejo de transacciones, observa detenidamente los registros y nota las clases AOP y Proxy creadas por el marco de Spring. El marco de Spring está utilizando un consejo Around para generar una clase proxy para CustomerManagerImpl y solo confirmar la transacción si el método devuelve correctamente. Si hay alguna excepción, simplemente revertirá toda la transacción. Te sugeriría que leas el Ejemplo de AOP de Spring para aprender más sobre el modelo de Programación Orientada a Aspectos. Eso es todo para el Ejemplo de Manejo de Transacciones de Spring, descarga el proyecto de muestra desde el siguiente enlace y juega con él para aprender más.

Descargar Proyecto de Manejo de Transacciones de Spring JDBC

Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example