Esempio di Gestione delle Transazioni in Spring con JDBC

Gestione delle transazioni Spring è una delle caratteristiche più ampiamente utilizzate e importanti del framework Spring. La gestione delle transazioni è un compito banale in qualsiasi applicazione enterprise. Abbiamo già appreso come utilizzare API JDBC per la gestione delle transazioni. Spring fornisce un ampio supporto per la gestione delle transazioni e aiuta gli sviluppatori a concentrarsi maggiormente sulla logica aziendale piuttosto che preoccuparsi dell’integrità dei dati in caso di eventuali guasti del sistema.

Gestione delle transazioni Spring

Alcuni dei vantaggi nell’utilizzare la Gestione delle Transazioni Spring sono:

  1. Supporto per la Gestione delle Transazioni Dichiarativa. In questo modello, Spring utilizza l’AOP sui metodi transazionali per fornire integrità dei dati. Questo è l’approccio preferito e funziona nella maggior parte dei casi.
  2. Supporto per la maggior parte delle API di transazione come JDBC, Hibernate, JPA, JDO, JTA, ecc. Tutto ciò che dobbiamo fare è utilizzare la classe di implementazione del gestore delle transazioni appropriato. Ad esempio, org.springframework.jdbc.datasource.DriverManagerDataSource per la gestione delle transazioni JDBC e org.springframework.orm.hibernate3.HibernateTransactionManager se stiamo utilizzando Hibernate come strumento ORM.
  3. Supporto per la gestione delle transazioni programmatica utilizzando TransactionTemplate o l’implementazione di PlatformTransactionManager.

La maggior parte delle funzionalità che vorremmo in un gestore delle transazioni è supportata dalla gestione delle transazioni dichiarativa, quindi useremmo questo approccio per il nostro progetto di esempio.

Esempio di gestione delle transazioni JDBC di Spring

Creeremo un semplice progetto Spring JDBC in cui aggiorneremo più tabelle in una singola transazione. La transazione dovrebbe essere confermata solo quando tutte le istruzioni JDBC vengono eseguite correttamente, altrimenti dovrebbe essere annullata per evitare inconsistenze nei dati. Se conosci la gestione delle transazioni JDBC, potresti sostenere che è possibile farlo facilmente impostando auto-commit su false per la connessione e, in base al risultato di tutte le istruzioni, confermare o annullare la transazione. Ovviamente è possibile farlo, ma ciò comporterà una grande quantità di codice boilerplate solo per la gestione delle transazioni. Inoltre, lo stesso codice sarà presente in tutti i punti in cui cerchiamo la gestione delle transazioni, causando un codice fortemente accoppiato e non manutenibile. La gestione dichiarativa delle transazioni di Spring affronta questi problemi utilizzando la programmazione orientata agli aspetti per ottenere un accoppiamento debole e evitare il codice boilerplate nella nostra applicazione. Vediamo come Spring lo fa con un esempio semplice. Prima di immergerci nel nostro progetto Spring, facciamo alcune impostazioni del database per il nostro utilizzo.

Gestione delle transazioni di Spring – Configurazione del database

Creeremo due tabelle per il nostro utilizzo e le aggiorneremo entrambe in una singola transazione.

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;

Potremmo definire qui la relazione chiave esterna dalla colonna id di Address alla colonna id di Customer, ma per semplicità non ho definito alcun vincolo qui. La nostra configurazione del database è pronta per il progetto di gestione delle transazioni di primavera, creiamo quindi un semplice progetto Maven di Spring nella Spring Tool Suite. La struttura finale del nostro progetto sarà simile all’immagine seguente. Esaminiamo uno per uno i vari pezzi, insieme forniranno un semplice esempio di gestione delle transazioni di primavera con JDBC.

Gestione delle transazioni di Spring – Dipendenze Maven

Dato che stiamo utilizzando l’API JDBC, dovremmo includere la dipendenza spring-jdbc nella nostra applicazione. Avremmo anche bisogno del driver del database MySQL per connetterci al database MySQL, quindi includeremo anche la dipendenza mysql-connector-java. L’artefatto spring-tx fornisce le dipendenze per la gestione delle transazioni, di solito viene incluso automaticamente da STS ma se non lo è, è necessario includerlo anche. Potresti vedere alcune altre dipendenze per il logging e i test di unità, tuttavia non ne faremo uso. Il nostro file pom.xml finale assomiglia al seguente codice.

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

Gestione delle transazioni di primavera – Classi di modello

Creeremo due Java Beans, Customer e Address, che mappano alle nostre tabelle.

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

Nota che il bean Customer ha Address come una delle sue variabili. Quando implementeremo il DAO per Customer, otterremo dati sia per il cliente che per la tabella dell’indirizzo e eseguiremo due query di inserimento separate per queste tabelle ed è per questo che abbiamo bisogno della gestione delle transazioni per evitare l’incoerenza dei dati.

Gestione delle transazioni di primavera – Implementazione del DAO

Implementiamo il DAO per il bean Customer, per semplicità avremo solo un metodo per inserire il record sia nelle tabelle del cliente che dell’indirizzo.

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

}

Nota che l’implementazione del CustomerDAO non si occupa della gestione delle transazioni. In questo modo stiamo ottenendo la separazione delle preoccupazioni perché a volte otteniamo implementazioni DAO da terze parti e non abbiamo il controllo su queste classi.

Gestione dichiarativa delle transazioni di primavera – Servizio

Creiamo un servizio client che utilizzerà l’implementazione CustomerDAO e fornirà la gestione delle transazioni durante l’inserimento dei record nelle tabelle dei clienti e degli indirizzi in un singolo metodo.

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 noti l’implementazione di CustomerManager, sta semplicemente utilizzando l’implementazione CustomerDAO per creare il cliente ma fornisce la gestione delle transazioni dichiarativa annotando il metodo createCustomer() con @Transactional. Questo è tutto ciò che dobbiamo fare nel nostro codice per ottenere i vantaggi della gestione delle transazioni di Spring. @Transactional può essere applicata sia sui metodi che sull’intera classe. Se desideri che tutti i tuoi metodi abbiano le funzionalità di gestione delle transazioni, dovresti annotare la tua classe con questa annotazione. Leggi di più sulle annotazioni su Tutorial sulle annotazioni Java. L’unico pezzo rimasto è collegare i bean di Spring per far funzionare l’esempio di gestione delle transazioni di Spring.

Gestione delle transazioni di primavera – Configurazione del bean

Creare un file di configurazione Spring Bean con il nome “spring.xml”. Lo utilizzeremo nel nostro programma di test per collegare i bean di Spring ed eseguire il nostro programma JDBC per testare la gestione delle transazioni.

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

Punti importanti da notare nel file di configurazione dei bean di Spring sono:

  • tx:annotation-driven viene utilizzato per indicare al contesto di Spring che stiamo utilizzando una configurazione di gestione delle transazioni basata su annotazioni. L’attributo transaction-manager viene utilizzato per fornire il nome del bean del gestore delle transazioni. Il valore predefinito di transaction-manager è transactionManager, ma lo sto ancora specificando per evitare confusione. L’attributo proxy-target-class viene utilizzato per indicare al contesto di Spring di utilizzare proxy basati su classi. Senza di esso, si otterrà un’eccezione durante l’esecuzione con un messaggio simile a Eccezione nel thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Il bean denominato ‘customerManager’ deve essere di tipo [com.journaldev.spring.jdbc.service.CustomerManagerImpl], ma è effettivamente di tipo [com.sun.proxy.$Proxy6]
  • Dato che stiamo utilizzando JDBC, stiamo creando un bean transactionManager di tipo org.springframework.jdbc.datasource.DataSourceTransactionManager. Questo è molto importante e dovremmo utilizzare la classe di implementazione del gestore delle transazioni corretta in base all’utilizzo della nostra API di transazione.
  • Il bean dataSource viene utilizzato per creare l’oggetto DataSource e dobbiamo fornire le proprietà di configurazione del database come driverClassName, url, username e password. Modificare questi valori in base alle impostazioni locali.
  • Stiamo iniettando dataSource nel bean customerDAO. Allo stesso modo stiamo iniettando il bean customerDAO nella definizione del bean customerManager.

La nostra configurazione è pronta, creiamo una semplice classe di test per testare la nostra implementazione della gestione delle transazioni.

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");
		// impostando un valore di più di 20 caratteri, in modo che si verifichi un'eccezione SQLException
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Nota che sto impostando esplicitamente il valore della colonna dell’indirizzo troppo lungo in modo da ottenere un’eccezione durante l’inserimento dei dati nella tabella degli indirizzi. Ora quando eseguiamo il nostro programma di test, otteniamo il seguente output.

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

Notare che il messaggio di log indica che i dati sono stati inseriti nella tabella dei clienti con successo, ma l’eccezione generata dal driver del database MySQL indica chiaramente che il valore è troppo lungo per la colonna dell’indirizzo. Ora, se controlli la tabella dei Clienti, non troverai alcuna riga, il che significa che la transazione è stata completamente annullata. Se ti stai chiedendo dove avviene la magia della gestione delle transazioni, guarda attentamente i log e nota le classi AOP e Proxy create dal framework Spring. Il framework Spring utilizza il consiglio Around per generare una classe proxy per CustomerManagerImpl e commitare la transazione solo se il metodo restituisce con successo. Se c’è un’eccezione, viene semplicemente annullata l’intera transazione. Ti suggerirei di leggere Spring AOP Example per saperne di più sul modello di Programmazione Orientata agli Aspetti. Questo è tutto per l’esempio di Gestione delle Transazioni di Spring, scarica il progetto di esempio dal link sottostante e gioca con esso per saperne di più.

Scarica il Progetto di Gestione delle Transazioni JDBC di Spring

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