An illustration of Spring Transaction Management using JDBC.

One of the most crucial and frequently utilized features of the Spring framework is Spring Transaction Management. Managing transactions is an essential responsibility in any enterprise application. Previously, we learned to handle transactions using the JDBC API. However, Spring offers comprehensive assistance for transaction management, allowing developers to concentrate on their business logic and alleviate concerns about data integrity during system failures.

Managing transactions in the Spring framework

There are several advantages of employing Spring Transaction Management:

    1. Declarative Transaction Management is supported by Spring where AOP is used to ensure data integrity in most cases.

 

    1. Most transaction APIs such as JDBC, Hibernate, JPA, JDO, JTA etc. are supported. We simply need to use the appropriate transaction manager implementation class. For instance, org.springframework.jdbc.datasource.DriverManagerDataSource for JDBC transaction management or org.springframework.orm.hibernate3.HibernateTransactionManager for Hibernate as the ORM tool.

 

    Programmatic transaction management is also supported by using TransactionTemplate or a PlatformTransactionManager implementation.

We will opt for Declarative transaction management for our example project as it supports most of the features we desire in a transaction manager.

Here’s a suggestion for a native paraphrase:

Example of JDBC Implementation for Spring Transaction Management

We will be creating a Spring JDBC project that involves updating multiple tables in one transaction. The transaction will only be committed if all the JDBC statements execute successfully; otherwise, it will rollback to prevent data inconsistency. If you are familiar with JDBC transaction management, you may argue that this can easily be achieved by setting auto-commit to false for the connection and deciding whether to commit or rollback based on the statements’ results. While this approach is possible, it would result in a significant amount of redundant code for transaction management. Moreover, this code would need to be repeated in all instances where transaction management is required, leading to tightly coupled and difficult-to-maintain code. Spring’s declarative transaction management solves these concerns by utilizing Aspect Oriented Programming, which offers loose coupling and eliminates the need for excessive code in our application. Let’s explore how Spring accomplishes this through a simple example. But before we delve into our Spring project, we need to perform some database setup for our purposes.

Managing transactions in Spring – Setting up the database.

We will generate two tables for our purpose and simultaneously update them within a single 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;

We have the option to establish a foreign-key relationship here, linking the Address id column to the Customer id column. However, to keep things simple, I have chosen not to define any constraints. Our Database setup is prepared for the spring transaction management project. Now, let’s create a basic Spring Maven Project in the Spring Tool Suite. The resulting project structure will resemble the image below. Now, let’s examine each component individually, as they will collectively demonstrate an example of spring transaction management with JDBC.

Maven Dependencies for Managing Spring Transactions

Because we are utilizing the JDBC API, it is necessary to incorporate the spring-jdbc dependency into our application. Additionally, in order to connect to a MySQL database, we will include the mysql-connector-java dependency. The spring-tx artifact provides transaction management dependencies, which are typically included automatically by STS. However, if they are not, they must be included as well. Although there may be other dependencies for logging and unit testing, we will not be utilizing any of them. The code snippet displayed below represents our final pom.xml file.

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

As of today, I have made sure to update the Spring versions, so please ensure that the MySQL database driver is compatible with your mysql installation.

Managing transactions in a Spring framework- Model Classes.

We are going to develop two Java Beans, namely Customer and Address, which will correspond with our tables.

 com.scdev.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;
	}
	
}
 com.scdev.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;
	}
	
}

When implementing the DAO for the Customer, we must take note that the Customer bean includes the Address as one of its variables. Consequently, when retrieving data for both the customer and address tables, we will need to execute two independent insert queries for each table. To ensure data consistency, it is crucial to have transaction management in place.

Implementing DAO with Transaction Management in Spring.

To simplify matters, we will focus on designing the DAO for the Customer bean and have only one method for inserting records in both the customer and address tables.

 com.scdev.spring.jdbc.dao;

import com.scdev.spring.jdbc.model.Customer;

public interface CustomerDAO {

	public void create(Customer customer);
}
 com.scdev.spring.jdbc.dao;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import com.scdev.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");
	}

}

It should be noted that the CustomerDAO implementation does not handle transaction management. By doing this, we ensure a separation of concerns as there are instances where we obtain DAO implementations from external sources and have no control over these classes.

Spring’s declarative transaction management for services.

We can develop a Customer Service that utilizes the CustomerDAO implementation and handles transaction management while inserting records into the customer and address tables within a single method.

 com.scdev.spring.jdbc.service;

import com.scdev.spring.jdbc.model.Customer;

public interface CustomerManager {

	public void createCustomer(Customer cust);
}
 com.scdev.spring.jdbc.service;

import org.springframework.transaction.annotation.Transactional;

import com.scdev.spring.jdbc.dao.CustomerDAO;
import com.scdev.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);
	}

}

Simply put, the CustomerManager implementation utilizes the CustomerDAO implementation to create customers. It achieves declarative transaction management by annotating the createCustomer() method with @Transactional. This annotation is enough to enjoy the advantages of Spring transaction management. You can apply @Transactional to individual methods or the entire class if you wish to enable transaction management for all methods. To understand annotations further, refer to the Java Annotations Tutorial. The only remaining step is to wire the spring beans to make the spring transaction management example functional.

Configuring Beans for Spring Transaction Management

Generate a Spring Bean Configuration file named “spring.xml”. This file will be utilized in our test program to establish connections between spring beans and execute our JDBC program for the purpose of testing transaction management.

<?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="scdev" />
		<property name="password" value="scdev123" />
	</bean>

	<bean id="customerDAO" class="com.scdev.spring.jdbc.dao.CustomerDAOImpl">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<bean id="customerManager" class="com.scdev.spring.jdbc.service.CustomerManagerImpl">
		<property name="customerDAO" ref="customerDAO"></property>
	</bean>

</beans>

Some key aspects to consider in the spring bean configuration file include:

  • tx:annotation-driven element is used to tell Spring context that we are using annotation based transaction management configuration. transaction-manager attribute is used to provide the transaction manager bean name. transaction-manager default value is transactionManager but I am still having it to avoid confusion. proxy-target-class attribute is used to tell Spring context to use class based proxies, without it you will get runtime exception with message such as Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.scdev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
  • Since we are using JDBC, we are creating transactionManager bean of type org.springframework.jdbc.datasource.DataSourceTransactionManager. This is very important and we should use proper transaction manager implementation class based on our transaction API use.
  • dataSource bean is used to create the DataSource object and we are required to provide the database configuration properties such as driverClassName, url, username and password. Change these values based on your local settings.
  • We are injecting dataSource into customerDAO bean. Similarly we are injecting customerDAO bean into customerManager bean definition.

Our configuration is complete, now we can proceed to create a straightforward test class for evaluating our implementation of transaction management.

 com.scdev.spring.jdbc.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.scdev.spring.jdbc.model.Address;
import com.scdev.spring.jdbc.model.Customer;
import com.scdev.spring.jdbc.service.CustomerManager;
import com.scdev.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");
		// setting value more than 20 chars, so that SQLException occurs
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Please observe that I intentionally made the address column value excessively long, causing an exception during the insertion of data into the Address table. As a result, when we execute our test program, we receive the subsequent 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.scdev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
	at com.scdev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
	at com.scdev.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.scdev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
	at com.scdev.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

The log message indicates that data was successfully inserted into the customer table, but the exception thrown by the MySQL database driver states that the value is too long for the address column. If you check the Customer table, you will not find any rows, indicating that the transaction has been completely rolled back. The transaction management is being handled by the AOP and Proxy classes created by the Spring framework. Spring is using Around advice to create a proxy class for CustomerManagerImpl and only commits the transaction if the method returns successfully. In case of any exceptions, the entire transaction is rolled back. I recommend reading the Spring AOP Example to learn more about Aspect Oriented Programming. That’s everything for the Spring Transaction Management Example. You can download the sample project from the link below and explore it further.

Get the Spring JDBC Transaction Management Project downloaded.

 

More tutorials

Commonly asked questions and answers for Hibernate interviews(Opens in a new browser tab)

LVM concepts, terminology, and procedures.(Opens in a new browser tab)

The Command design pattern.(Opens in a new browser tab)

Commonly questions and answers for a spring interview.(Opens in a new browser tab)

An instructional tutorial on the Jackson JSON Java Parser API.(Opens in a new browser tab)

Leave a Reply 0

Your email address will not be published. Required fields are marked *