Exemple de gestion des transactions Spring JDBC

La gestion des transactions Spring est l’une des fonctionnalités les plus largement utilisées et importantes du framework Spring. La gestion des transactions est une tâche trivial dans n’importe quelle application d’entreprise. Nous avons déjà appris comment utiliser l’API JDBC pour la gestion des transactions. Spring offre un support étendu pour la gestion des transactions et aide les développeurs à se concentrer davantage sur la logique métier plutôt que de s’inquiéter de l’intégrité des données en cas de défaillance du système.

Gestion des transactions Spring

Certains des avantages de l’utilisation de la gestion des transactions Spring sont :

  1. Support de la gestion des transactions déclarative. Dans ce modèle, Spring utilise l’AOP sur les méthodes transactionnelles pour assurer l’intégrité des données. C’est l’approche préférée et fonctionne dans la plupart des cas.
  2. Prise en charge de la plupart des API de transaction telles que JDBC, Hibernate, JPA, JDO, JTA, etc. Tout ce que nous avons à faire est d’utiliser la classe d’implémentation du gestionnaire de transactions approprié. Par exemple org.springframework.jdbc.datasource.DriverManagerDataSource pour la gestion des transactions JDBC et org.springframework.orm.hibernate3.HibernateTransactionManager si nous utilisons Hibernate comme outil ORM.
  3. Prise en charge de la gestion de transactions programmatique en utilisant TransactionTemplate ou l’implémentation de PlatformTransactionManager.

La plupart des fonctionnalités que nous voudrions dans un gestionnaire de transactions sont prises en charge par la gestion de transactions déclarative, nous utiliserions donc cette approche pour notre projet exemple.

Exemple de gestion de transactions Spring JDBC

Nous allons créer un projet Spring JDBC simple dans lequel nous mettrons à jour plusieurs tables dans une seule transaction. La transaction ne devrait être validée que lorsque toutes les instructions JDBC s’exécutent avec succès, sinon elle devrait être annulée pour éviter les incohérences de données. Si vous connaissez la gestion des transactions JDBC, vous pourriez argumenter que nous pouvons le faire facilement en définissant auto-commit sur false pour la connexion et en fonction du résultat de toutes les instructions, soit valider soit annuler la transaction. Bien sûr, nous pouvons le faire, mais cela entraînera beaucoup de code boilerplate uniquement pour la gestion des transactions. De plus, le même code sera présent à tous les endroits où nous recherchons la gestion des transactions, ce qui entraînera un couplage étroit et un code non maintenable. La gestion déclarative des transactions Spring aborde ces problématiques en utilisant la programmation orientée aspect pour obtenir un couplage lâche et éviter le code boilerplate dans notre application. Voyons comment Spring le fait avec un exemple simple. Avant de plonger dans notre projet Spring, effectuons une configuration de base de données pour notre utilisation.

Gestion des transactions Spring – Configuration de la base de données

Nous allons créer deux tables pour notre utilisation et les mettre à jour toutes les deux dans une seule transaction.

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;

Nous pourrions définir une relation de clé étrangère ici du champ id de l’adresse au champ id du client, mais pour des raisons de simplicité, je n’ai aucune contrainte définie ici. Notre configuration de base de données est prête pour le projet de gestion des transactions de printemps, créons un projet Spring Maven simple dans Spring Tool Suite. La structure finale de notre projet ressemblera à l’image ci-dessous. Examinons chaque élément un par un, ensemble ils fourniront un exemple simple de gestion des transactions Spring avec JDBC.

Gestion des transactions Spring – Dépendances Maven

Comme nous utilisons l’API JDBC, nous devons inclure la dépendance spring-jdbc dans notre application. Nous aurons également besoin du pilote de base de données MySQL pour nous connecter à la base de données MySQL, donc nous inclurons également la dépendance mysql-connector-java. L’artefact spring-tx fournit les dépendances de gestion des transactions, généralement inclus automatiquement par STS, mais si ce n’est pas le cas, vous devez également l’inclure. Vous pourriez voir d’autres dépendances pour le journalisation et les tests unitaires, cependant, nous n’utiliserons aucune d’entre elles. Notre fichier pom.xml final ressemble au code ci-dessous.

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

Gestion des transactions Spring – Classes de modèle

Nous allons créer deux Java Beans, Customer et Address, qui seront associés à nos tables.

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

Remarquez que le bean Customer a Address comme l’une de ses variables. Lorsque nous implémenterons le DAO pour Customer, nous obtiendrons des données pour les tables customer et address, et nous exécuterons deux requêtes d’insertion distinctes pour ces tables. C’est pourquoi nous avons besoin d’une gestion des transactions pour éviter les incohérences de données.

Gestion des transactions Spring – Implémentation du DAO

Implémentons le DAO pour le bean Customer. Pour des raisons de simplicité, nous aurons simplement une méthode pour insérer un enregistrement dans les tables customer et address.

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

}

Remarquez que l’implémentation de CustomerDAO ne prend pas en charge la gestion des transactions. De cette manière, nous réalisons une séparation des préoccupations car parfois nous obtenons des implémentations DAO de tiers et nous n’avons pas le contrôle sur ces classes.

Gestion des transactions déclaratives de printemps – Service

Créons un service client qui utilisera l’implémentation CustomerDAO et assurera la gestion des transactions lors de l’insertion d’enregistrements dans les tables customer et address dans une seule méthode.

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 vous remarquez l’implémentation CustomerManager, elle utilise simplement l’implémentation CustomerDAO pour créer le client mais assure la gestion des transactions de manière déclarative en annotant la méthode createCustomer() avec l’annotation @Transactional. C’est tout ce que nous devons faire dans notre code pour bénéficier de la gestion des transactions de Spring. @Transactional L’annotation peut être appliquée sur des méthodes ainsi que sur toute la classe. Si vous voulez que toutes vos méthodes aient des fonctionnalités de gestion des transactions, vous devriez annoter votre classe avec cette annotation. En savoir plus sur les annotations dans le Tutoriel sur les annotations Java. La seule partie restante est de câbler les beans Spring pour que l’exemple de gestion des transactions de printemps fonctionne.

Gestion des transactions de printemps – Configuration des beans

Créez un fichier de configuration de bean Spring avec le nom « spring.xml ». Nous l’utiliserons dans notre programme de test pour câbler les beans Spring et exécuter notre programme JDBC afin de tester la gestion des transactions.

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

Les points importants à noter dans le fichier de configuration des beans Spring sont:

  • tx:annotation-driven élément est utilisé pour indiquer au contexte Spring que nous utilisons une configuration de gestion des transactions basée sur les annotations. transaction-manager attribut est utilisé pour fournir le nom du bean gestionnaire de transactions. la valeur par défaut de transaction-manager est transactionManager mais je l’ai toujours pour éviter toute confusion. proxy-target-class attribut est utilisé pour indiquer au contexte Spring d’utiliser des proxies basés sur la classe, sans quoi vous obtiendrez une exception d’exécution avec un message tel que Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Le bean nommé ‘customerManager’ doit être de type [com.journaldev.spring.jdbc.service.CustomerManagerImpl], mais était en fait de type [com.sun.proxy.$Proxy6]
  • Comme nous utilisons JDBC, nous créons un bean transactionManager de type org.springframework.jdbc.datasource.DataSourceTransactionManager. Ceci est très important et nous devrions utiliser la bonne classe d’implémentation de gestionnaire de transactions en fonction de notre utilisation de l’API de transaction.
  • dataSource bean est utilisé pour créer l’objet DataSource et nous devons fournir les propriétés de configuration de la base de données telles que driverClassName, url, username et password. Modifiez ces valeurs en fonction de vos paramètres locaux.
  • Nous injectons la dataSource dans le bean customerDAO. De même, nous injectons le bean customerDAO dans la définition du bean customerManager.

Notre configuration est prête, créons une classe de test simple pour tester notre implémentation de gestion de transaction.

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");
		// définir une valeur de plus de 20 caractères, afin qu'une exception SQLException se produise
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Remarquez que je définis explicitement la valeur de la colonne d’adresse trop longue afin que nous obtenions une exception lors de l’insertion de données dans la table Address. Maintenant, lorsque nous exécutons notre programme de test, nous obtenons la sortie suivante.

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

Remarquez que le message de journal indique que les données ont été insérées dans la table des clients avec succès, mais l’exception lancée par le pilote de base de données MySQL indique clairement que la valeur est trop longue pour la colonne de l’adresse. Maintenant, si vous vérifiez la table des clients, vous ne trouverez aucune ligne, ce qui signifie que la transaction est complètement annulée. Si vous vous demandez où se trouve la magie de la gestion des transactions, regardez attentivement les journaux et remarquez les classes AOP et Proxy créées par le framework Spring. Le framework Spring utilise un conseil Around pour générer une classe proxy pour CustomerManagerImpl et ne commit la transaction que si la méthode retourne avec succès. En cas d’exception, il annule simplement toute la transaction. Je vous suggère de lire Exemple de AOP Spring pour en savoir plus sur le modèle de programmation orientée aspect. C’est tout pour l’exemple de gestion des transactions de Spring, téléchargez le projet d’exemple à partir du lien ci-dessous et amusez-vous avec pour en apprendre davantage.

Télécharger le projet de gestion des transactions JDBC Spring

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