Exemplo de Gerenciamento de Transações Spring com JDBC

Gerenciamento de Transações do Spring é uma das características mais amplamente utilizadas e importantes do framework Spring. O Gerenciamento de Transações é uma tarefa trivial em qualquer aplicação empresarial. Já aprendemos como usar a API JDBC para o Gerenciamento de Transações. O Spring oferece suporte extensivo para o gerenciamento de transações e ajuda os desenvolvedores a se concentrarem mais na lógica de negócios, em vez de se preocuparem com a integridade dos dados em caso de falhas do sistema.

Gerenciamento de Transações do Spring

Alguns dos benefícios de usar o Gerenciamento de Transações do Spring são:

  1. Suporte para Gerenciamento de Transações Declarativo. Neste modelo, o Spring utiliza AOP sobre os métodos transacionais para fornecer integridade de dados. Esta é a abordagem preferida e funciona na maioria dos casos.
  2. Suporte para a maioria das APIs de transação, como JDBC, Hibernate, JPA, JDO, JTA, etc. Tudo o que precisamos fazer é usar a classe de implementação apropriada do gerenciador de transações. Por exemplo, org.springframework.jdbc.datasource.DriverManagerDataSource para o gerenciamento de transações JDBC e org.springframework.orm.hibernate3.HibernateTransactionManager se estivermos usando o Hibernate como ferramenta ORM.
  3. Suporte para o gerenciamento de transações programático usando TransactionTemplate ou a implementação de PlatformTransactionManager.

A maioria dos recursos desejados em um gerenciador de transações é suportada pelo gerenciamento de transações declarativo, então usaríamos essa abordagem para o nosso projeto de exemplo.

Exemplo de Gerenciamento de Transações com Spring JDBC

Vamos criar um projeto simples de Spring JDBC, onde atualizaremos várias tabelas em uma única transação. A transação deve ser confirmada apenas quando todas as instruções JDBC forem executadas com sucesso; caso contrário, deve ser revertida para evitar inconsistência nos dados. Se você conhece o gerenciamento de transações JDBC, pode argumentar que podemos fazer isso facilmente definindo o autocommit como falso para a conexão e, com base no resultado de todas as instruções, confirmar ou reverter a transação. Obviamente, podemos fazer isso, mas resultará em muito código redundante apenas para o gerenciamento de transações. Além disso, o mesmo código estará presente em todos os lugares onde procuramos o gerenciamento de transações, causando um código fortemente acoplado e difícil de manter. O gerenciamento declarativo de transações do Spring aborda essas preocupações, utilizando a Programação Orientada a Aspectos para alcançar um acoplamento flexível e evitar código redundante em nossa aplicação. Vamos ver como o Spring faz isso com um exemplo simples. Antes de mergulharmos em nosso projeto Spring, vamos fazer algumas configurações de banco de dados para nosso uso.

Gerenciamento de Transações do Spring – Configuração do Banco de Dados

Vamos criar duas tabelas para nosso uso e atualizá-las em uma única transação.

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;

Podemos definir a relação de chave estrangeira aqui da coluna id do endereço para a coluna id do cliente, mas para simplicidade não tenho nenhuma restrição definida aqui. Nossa configuração do banco de dados está pronta para o projeto de gerenciamento de transações da primavera, vamos criar um Projeto Maven Spring simples no Spring Tool Suite. A estrutura final do nosso projeto será parecida com a imagem abaixo. Vamos analisar cada uma das partes uma por uma, juntas elas fornecerão um exemplo simples de gerenciamento de transações da primavera com JDBC.

Gerenciamento de Transações da Primavera – Dependências do Maven

Como estamos usando a API JDBC, teríamos que incluir a dependência spring-jdbc em nossa aplicação. Também precisaremos do driver do banco de dados MySQL para nos conectarmos ao banco de dados mysql, então incluiremos a dependência mysql-connector-java também. O artefato spring-tx fornece as dependências de gerenciamento de transações, geralmente é incluído automaticamente pelo STS, mas se não for, você também precisa incluí-lo. Você pode ver algumas outras dependências para registro e teste de unidade, no entanto, não usaremos nenhuma delas. Nosso arquivo pom.xml final parece com o código abaixo.

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

Gerenciamento de Transações Spring – Classes de Modelo

Vamos criar dois Java Beans, Cliente e Endereço, que irão mapear para nossas tabelas.

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;
	}
	
}

Observe que o bean Cliente tem Endereço como uma de suas variáveis. Quando implementarmos o DAO para Cliente, iremos obter dados tanto para o cliente quanto para a tabela de endereços e executaremos duas consultas de inserção separadas para essas tabelas, e é por isso que precisamos de gerenciamento de transações para evitar inconsistência de dados.

Gerenciamento de Transações Spring – Implementação do DAO

Vamos implementar o DAO para o bean Cliente, para simplicidade, teremos apenas um método para inserir registros em ambas as tabelas de cliente e endereço.

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");
	}

}

Observe que a implementação do CustomerDAO não está cuidando do gerenciamento de transações. Dessa forma, estamos alcançando a separação de preocupações porque às vezes obtemos implementações de DAO de terceiros e não temos controle sobre essas classes.

Gerenciamento Declarativo de Transações Spring – Serviço

Vamos criar um Serviço de Cliente que utilizará a implementação do CustomerDAO e fornecerá gerenciamento de transações ao inserir registros nas tabelas de cliente e endereço em um único 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);
	}

}

Se você observar a implementação do CustomerManager, ela está apenas utilizando a implementação do CustomerDAO para criar o cliente, mas fornece gerenciamento de transações de forma declarativa através da anotação @Transactional no método createCustomer(). Isso é tudo que precisamos fazer em nosso código para obter os benefícios do gerenciamento de transações do Spring. A anotação @Transactional pode ser aplicada em métodos individuais ou em toda a classe. Se você deseja que todos os seus métodos tenham recursos de gerenciamento de transações, deve anotar sua classe com esta anotação. Saiba mais sobre anotações em Tutorial de Anotações Java. A única parte que resta é conectar os beans do Spring para fazer o exemplo de gerenciamento de transações do Spring funcionar.

Gerenciamento de Transações Spring – Configuração do Bean

Crie um arquivo de configuração de feijão Spring com o nome “spring.xml”. Vamos usar isso em nosso programa de teste para conectar os feijões Spring e executar nosso programa JDBC para testar o gerenciamento de transações.

<?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>

Pontos importantes a serem observados no arquivo de configuração de feijão Spring são:

  • tx:annotation-driven elemento é usado para informar ao contexto Spring que estamos usando uma configuração baseada em anotações para o gerenciamento de transações. O atributo transaction-manager é usado para fornecer o nome do feijão gerenciador de transações. O valor padrão do gerenciador de transações é transactionManager, mas ainda o estou mantendo para evitar confusão. O atributo proxy-target-class é usado para informar ao contexto Spring para usar proxies baseados em classe, sem ele você obterá uma exceção em tempo de execução com uma mensagem como Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.journaldev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
  • Como estamos usando JDBC, estamos criando o feijão transactionManager do tipo org.springframework.jdbc.datasource.DataSourceTransactionManager. Isso é muito importante e devemos usar a classe de implementação correta do gerenciador de transações com base no uso de nossa API de transações.
  • O feijão dataSource é usado para criar o objeto DataSource e devemos fornecer as propriedades de configuração do banco de dados, como driverClassName, url, nome de usuário e senha. Altere esses valores com base em suas configurações locais.
  • Estamos injetando o dataSource no bean customerDAO. Da mesma forma, estamos injetando o bean customerDAO na definição do bean customerManager.

Nossa configuração está pronta, vamos criar uma classe de teste simples para testar nossa implementação de gerenciamento de transações.

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");
		// definindo um valor com mais de 20 caracteres, para que ocorra SQLException
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Observe que estou configurando explicitamente o valor da coluna de endereço com um valor muito longo para que obtenhamos uma exceção ao inserir dados na tabela de endereços. Agora, ao executarmos nosso programa de teste, obtemos a seguinte saída.

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

Observe que a mensagem de log informa que os dados foram inseridos com sucesso na tabela de clientes, mas a exceção lançada pelo driver do banco de dados MySQL indica claramente que o valor é muito longo para a coluna de endereço. Agora, se você verificar a tabela de clientes, não encontrará nenhuma linha lá, o que significa que a transação foi totalmente revertida. Se você estiver se perguntando onde está acontecendo a mágica do gerenciamento de transações, examine os logs com atenção e observe as classes AOP e Proxy criadas pelo framework Spring. O Spring está usando o conselho Around para gerar uma classe proxy para CustomerManagerImpl e só está confirmando a transação se o método retornar com sucesso. Se houver alguma exceção, ele apenas reverte a transação inteira. Sugiro que leia Exemplo de AOP do Spring para saber mais sobre o modelo de Programação Orientada a Aspectos. Isso é tudo para o Exemplo de Gerenciamento de Transações do Spring, faça o download do projeto de exemplo no link abaixo e brinque com ele para aprender mais.

Baixar Projeto de Gerenciamento de Transações do Spring JDBC

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