Gerenciamento de Transações Spring é uma das características mais amplamente utilizadas e importantes do framework Spring. O Gerenciamento de Transações é uma tarefa trivial em qualquer aplicativo empresarial. Já aprendemos como usar a API JDBC para Gerenciamento de Transações. O Spring fornece amplo suporte para 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 no sistema.
Gerenciamento de Transações Spring
Alguns dos benefícios de usar o Gerenciamento de Transações Spring são:
- 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.
- 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 eorg.springframework.orm.hibernate3.HibernateTransactionManager
se estivermos usando o Hibernate como ferramenta ORM. - Suporte para o gerenciamento programático de transações usando
TransactionTemplate
ou a implementação dePlatformTransactionManager
.
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 e JDBC
Vamos criar um projeto simples usando o 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ê estiver familiarizado com o gerenciamento de transações JDBC, poderá argumentar que podemos fazer isso facilmente, configurando o auto-commit como falso para a conexão e, com base no resultado de todas as instruções, confirmar ou reverter a transação. Claro, podemos fazer isso, mas resultará em muito código repetitivo 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 acoplamento forte e um código difícil de manter. O gerenciamento de transações declarativo do Spring aborda essas preocupações usando a Programação Orientada a Aspectos para alcançar um acoplamento mais solto e evitar código redundante em nossa aplicação. Vamos ver como o Spring faz isso com um exemplo simples. Antes de entrarmos em nosso projeto Spring, vamos fazer algumas configurações no banco de dados para o nosso uso.
Gerenciamento de Transações no Spring – Configuração do Banco de Dados
Vamos criar duas tabelas para o nosso uso e atualizar ambas 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;
Poderíamos definir a relação de chave estrangeira aqui da coluna id do endereço para a coluna id do cliente, mas para simplificar, não tenho nenhuma restrição definida aqui. Nossa configuração de banco de dados está pronta para o projeto de gerenciamento de transações da primavera, vamos criar um simples Projeto Maven da Primavera no Spring Tool Suite. A estrutura final do nosso projeto parecerá com a imagem abaixo. Vamos examinar cada uma das peças 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 Maven
Como estamos usando a API JDBC, teríamos que incluir a dependência spring-jdbc em nossa aplicação. Também precisaríamos do driver do banco de dados MySQL para conectar ao banco de dados mysql, então incluiremos também a dependência mysql-connector-java. 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ê precisará incluí-lo também. Você pode ver algumas outras dependências para logging e testes de unidade, no entanto, não estaremos usando nenhuma delas. Nosso arquivo pom.xml final fica como 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 da Primavera – Classes do Modelo
Vamos criar duas Java Beans, Cliente e Endereço, que mapearão 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, obteremos dados tanto para a tabela de cliente quanto para a de endereço e executaremos duas consultas de inserção separadas para essas tabelas, e é por isso que precisamos do gerenciamento de transações para evitar inconsistência de dados.
Gerenciamento de Transações da Primavera – Implementação do DAO
Vamos implementar o DAO para o bean Cliente, para simplicidade, teremos apenas um método para inserir registro 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. Desta forma, estamos alcançando a separação de preocupações, pois às vezes obtemos implementações de DAO de terceiros e não temos controle sobre essas classes.
Gerenciamento de Transações Declarativas da Primavera – Serviço
Vamos criar um Serviço de Cliente que utilizará a implementação 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 CustomerManager, ela está apenas utilizando a implementação CustomerDAO para criar o cliente, mas fornece gerenciamento de transações declarativas por meio da anotação do método createCustomer() com a anotação @Transactional
. Isso é tudo o 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, bem como 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 restante é conectar os beans do Spring para fazer o exemplo de gerenciamento de transações do Spring funcionar.
Gerenciamento de Transações da Primavera – Configuração de Bean
Crie um arquivo de configuração de Spring Bean com o nome “spring.xml”. Usaremos isso em nosso programa de teste para conectar os beans do 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 do Spring Bean são:
- O elemento tx:annotation-driven é usado para informar ao contexto do Spring que estamos usando uma configuração de gerenciamento de transações baseada em anotações. O atributo transaction-manager é usado para fornecer o nome do bean do gerenciador de transações. O valor padrão do transaction-manager é transactionManager, mas ainda estou usando-o para evitar confusão. O atributo proxy-target-class é usado para informar ao contexto do Spring para usar proxies baseados em classe; sem ele, você receberá 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 bean 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 bean 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 uma SQLException
address.setAddress("Albany Dr, San Jose, CA 95129");
customer.setAddress(address);
return customer;
}
}
Observe que estou definindo explicitamente o valor da coluna de endereço como muito longo para que recebamos uma exceção ao inserir dados na tabela de Endereço. 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 indica que os dados foram inseridos na tabela de clientes com sucesso, mas a exceção lançada pelo driver do banco de dados MySQL claramente diz 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, observe atentamente os logs e perceba as classes AOP e Proxy criadas pelo framework Spring. O framework Spring está usando conselhos Around para gerar uma classe proxy para CustomerManagerImpl e apenas confirmar a transação se o método retornar com sucesso. Se houver alguma exceção, ela simplesmente desfaz toda a transação. Sugiro que você leia o Exemplo de AOP do Spring para aprender 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 amostra no link abaixo e explore-o para aprender mais.
Baixe o Projeto de Gerenciamento de Transações do Spring JDBC
Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example