Spring Transaction Management is een van de meest gebruikte en belangrijke functies van het Spring-framework. Transactiebeheer is een triviaal taak in elke bedrijfsapplicatie. We hebben al geleerd hoe we de JDBC API kunnen gebruiken voor transactiebeheer. Spring biedt uitgebreide ondersteuning voor transactiebeheer en helpt ontwikkelaars zich meer te richten op de bedrijfslogica in plaats van zich zorgen te maken over de integriteit van gegevens in geval van systeemstoringen.
Spring Transaction Management
Enkele voordelen van het gebruik van Spring Transaction Management zijn:
- Ondersteuning voor declaratief transactiebeheer. In dit model gebruikt Spring AOP over de transactionele methoden om gegevensintegriteit te bieden. Dit is de voorkeursaanpak en werkt in de meeste gevallen.
- Ondersteuning voor de meeste transactie-API’s zoals JDBC, Hibernate, JPA, JDO, JTA, enz. Het enige wat we moeten doen is de juiste implementatieklasse voor transactiebeheer gebruiken. Bijvoorbeeld
org.springframework.jdbc.datasource.DriverManagerDataSource
voor JDBC-transactiebeheer enorg.springframework.orm.hibernate3.HibernateTransactionManager
als we Hibernate als ORM-tool gebruiken. - Ondersteuning voor programmatisch transactiebeheer door gebruik te maken van
TransactionTemplate
ofPlatformTransactionManager
implementatie.
De meeste functies die we zouden willen in een transactiebeheerder worden ondersteund door Declaratief transactiebeheer, dus we zouden deze aanpak gebruiken voor ons voorbeeldproject.
Spring Transaction Management JDBC Voorbeeld
We zullen een eenvoudig Spring JDBC-project maken waarbij we meerdere tabellen in één transactie zullen bijwerken. De transactie moet alleen worden bevestigd wanneer alle JDBC-statements succesvol worden uitgevoerd, anders moet deze worden teruggedraaid om inconsistentie van gegevens te voorkomen. Als je bekend bent met JDBC-transactiebeheer, zou je kunnen beweren dat we het gemakkelijk kunnen doen door auto-commit op false in te stellen voor de verbinding en op basis van het resultaat van alle statements, de transactie kunnen bevestigen of terugdraaien. Uiteraard kunnen we dat doen, maar dat zal resulteren in veel boilerplate-code alleen voor transactiebeheer. Ook zal dezelfde code aanwezig zijn op alle plaatsen waar we transactiebeheer nodig hebben, waardoor er sprake is van sterk gekoppelde en niet-onderhoudbare code. Spring declaratief transactiebeheer behandelt deze zorgen door Aspect Oriented Programming te gebruiken om losse koppeling te bereiken en boilerplate-code in onze applicatie te vermijden. Laten we eens kijken hoe Spring dit doet aan de hand van een eenvoudig voorbeeld. Voordat we ons in ons Spring-project storten, laten we enkele database-instellingen voor ons gebruik doen.
Spring Transactiebeheer – Database-instellingen
We zullen twee tabellen maken voor ons gebruik en beide in één transactie bijwerken.
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;
We kunnen hier een vreemde-sleutel relatie definiëren vanuit de kolom Address id naar de kolom Customer id, maar voor de eenvoud heb ik hier geen beperking gedefinieerd. Onze databaseopstelling is klaar voor het project voor spring transactiebeheer, laten we een eenvoudig Spring Maven-project maken in de Spring Tool Suite. Onze uiteindelijke projectstructuur zal eruit zien zoals op onderstaande afbeelding. Laten we elk van de onderdelen één voor één bekijken, samen zullen ze een eenvoudig voorbeeld van spring transactiebeheer met JDBC bieden.
Spring Transactiebeheer – Maven Afhankelijkheden
Aangezien we de JDBC API gebruiken, moeten we de afhankelijkheid spring-jdbc in onze applicatie opnemen. We hebben ook de MySQL database driver nodig om verbinding te maken met de MySQL database, dus we zullen ook de afhankelijkheid mysql-connector-java opnemen. Het artefact spring-tx biedt afhankelijkheden voor transactiebeheer, meestal wordt dit automatisch opgenomen door STS, maar als dat niet het geval is, moet je het ook opnemen. Je zou enkele andere afhankelijkheden kunnen zien voor logging en unit testing, maar we zullen geen van beide gebruiken. Ons uiteindelijke pom.xml-bestand ziet eruit zoals de onderstaande code.
<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.
Spring Transaction Management – Model Klassen
We zullen twee Java Beans maken, Customer en Address, die zullen worden gekoppeld aan onze tabellen.
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;
}
}
Merk op dat de Customer bean Address als een van zijn variabelen heeft. Wanneer we DAO voor Customer implementeren, zullen we gegevens krijgen voor zowel de klant- als de adresstabel en zullen we twee afzonderlijke invoegquery’s uitvoeren voor deze tabellen en daarom hebben we transactiebeheer nodig om inconsistentie van gegevens te voorkomen.
Spring Transaction Management – DAO-implementatie
Laten we de DAO voor Customer bean implementeren, voor eenvoud hebben we slechts één methode om een record in zowel de klant- als de adres tabellen in te voegen.
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");
}
}
Merk op dat de implementatie van CustomerDAO niet zorgt voor transactiebeheer. Op deze manier bereiken we scheiding van zorgen omdat we soms DAO-implementaties van derden krijgen en we geen controle hebben over deze klassen.
Spring Declaratieve Transactiebeheer – Service
Laten we een Klantenservice maken die de implementatie van CustomerDAO zal gebruiken en transactiebeheer zal bieden bij het invoegen van gegevens in de klant- en adres tabellen in één enkele methode.
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);
}
}
Als je naar de implementatie van CustomerManager kijkt, gebruikt het gewoon de implementatie van CustomerDAO om de klant te maken, maar biedt het declaratief transactiebeheer door de createCustomer() methode te annoteren met @Transactional
. Dat is alles wat we moeten doen in onze code om de voordelen van Spring transactiebeheer te krijgen. @Transactional annotatie kan worden toegepast op methoden en op hele klassen. Als je wilt dat al je methoden transactiebeheerfuncties hebben, moet je je klasse met deze annotatie annoteren. Lees meer over annotaties in de Java Annotations Tutorial. Het enige dat nog resteert is het bedraden van Spring beans om het voorbeeld van Spring transactiebeheer te laten werken.
Spring Transactiebeheer – Bean Configuratie
Maak een Spring Bean-configuratiebestand met de naam “spring.xml”. We zullen dit gebruiken in ons testprogramma om Spring-beans te verbinden en ons JDBC-programma uit te voeren om transactiebeheer te testen.
<?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>
Belangrijke punten om op te merken in het Spring Bean-configuratiebestand zijn:
- Het element tx:annotation-driven wordt gebruikt om de Spring-context te vertellen dat we configuratie voor transactiebeheer op basis van annotaties gebruiken. Het attribuut transaction-manager wordt gebruikt om de bean-naam van de transactiemanager op te geven. De standaardwaarde voor transaction-manager is transactionManager, maar ik houd het nog steeds om verwarring te voorkomen. Het attribuut proxy-target-class wordt gebruikt om de Spring-context te vertellen om op klassen gebaseerde proxies te gebruiken. Zonder dit krijg je een runtime-uitzondering met een bericht zoals Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean met de naam ‘customerManager’ moet van het type [com.journaldev.spring.jdbc.service.CustomerManagerImpl] zijn, maar was eigenlijk van het type [com.sun.proxy.$Proxy6]
- Aangezien we JDBC gebruiken, maken we een transactionManager-bean van het type
org.springframework.jdbc.datasource.DataSourceTransactionManager
. Dit is zeer belangrijk en we moeten de juiste implementatieklasse voor de transactiemanager gebruiken op basis van ons gebruik van de transactie-API. - De bean dataSource wordt gebruikt om het DataSource-object te maken en we moeten de configuratie-eigenschappen van de database opgeven, zoals driverClassName, url, gebruikersnaam en wachtwoord. Wijzig deze waarden op basis van uw lokale instellingen.
- We injecteren de dataSource in de customerDAO bean. Op dezelfde manier injecteren we de customerDAO bean in de customerManager bean definitie.
Onze opstelling is klaar, laten we een eenvoudige testklasse maken om onze transactiebeheerimplementatie te testen.
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");
// waarde instellen van meer dan 20 tekens, zodat SQLException optreedt
address.setAddress("Albany Dr, San Jose, CA 95129");
customer.setAddress(address);
return customer;
}
}
Let op dat ik expliciet de adreskolomwaarde te lang instel, zodat we een uitzondering krijgen bij het invoegen van gegevens in de Adres-tabel. Als we nu ons testprogramma uitvoeren, krijgen we de volgende uitvoer.
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
Let op: het logbericht meldt dat de gegevens succesvol zijn ingevoegd in de klantentabel, maar de uitzondering die wordt gegenereerd door de MySQL-database driver geeft duidelijk aan dat de waarde te lang is voor de adreskolom. Als je nu de Klantentabel controleert, zul je daar geen enkele rij vinden, wat betekent dat de transactie volledig is teruggedraaid. Als je je afvraagt waar de magie van transactiebeheer plaatsvindt, kijk dan goed naar de logs en let op de AOP- en Proxy-klassen die zijn gemaakt door het Spring-framework. Het Spring-framework gebruikt Around-advies om een proxyklasse te genereren voor CustomerManagerImpl en de transactie alleen te bevestigen als de methode succesvol retourneert. Als er een uitzondering is, wordt de hele transactie teruggedraaid. Ik zou je aanraden om Spring AOP-voorbeeld te lezen om meer te weten te komen over het model van Aspect Oriented Programming. Dat is alles voor het voorbeeld van Spring Transaction Management. Download het voorbeeldproject vanaf de onderstaande link en experimenteer ermee om meer te leren.
Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example