Пример управления транзакциями Spring JDBC

Управление транзакциями в Spring – одна из самых широко используемых и важных функций в Spring framework. Управление транзакциями является тривиальной задачей в любом предприятии. Мы уже изучили, как использовать JDBC API для управления транзакциями. Spring предоставляет обширную поддержку управления транзакциями и помогает разработчикам сосредотачиваться больше на бизнес-логике, а не беспокоиться о целостности данных в случае сбоев системы.

Управление транзакциями в Spring

Некоторые из преимуществ использования управления транзакциями в Spring:

  1. Поддержка декларативного управления транзакциями. В этой модели Spring использует AOP над транзакционными методами для обеспечения целостности данных. Этот подход является предпочтительным и работает в большинстве случаев.
  2. Поддержка большинства API для транзакций, таких как JDBC, Hibernate, JPA, JDO, JTA и т. д. Все, что нам нужно сделать, это использовать соответствующий класс реализации менеджера транзакций. Например, org.springframework.jdbc.datasource.DriverManagerDataSource для управления транзакциями JDBC и org.springframework.orm.hibernate3.HibernateTransactionManager при использовании Hibernate в качестве ORM-инструмента.
  3. Поддержка программного управления транзакциями с использованием TransactionTemplate или реализации PlatformTransactionManager.

Большинство функций, которые мы хотели бы видеть в менеджере транзакций, поддерживаются декларативным управлением транзакциями, поэтому мы будем использовать этот подход для нашего примерного проекта.

Пример управления транзакциями JDBC в Spring

Мы создадим простой проект Spring JDBC, в котором мы будем обновлять несколько таблиц в одной транзакции. Транзакция должна выполняться только в том случае, если все операторы JDBC выполняются успешно, иначе она должна быть отменена, чтобы избежать несогласованности данных. Если вы знакомы с управлением транзакциями JDBC, вы могли бы возразить, что мы можем легко сделать это, установив автоматическое подтверждение на false для соединения и, исходя из результата всех операторов, либо подтвердить, либо откатить транзакцию. Конечно, мы можем это сделать, но это приведет к большому количеству шаблонного кода только для управления транзакциями. Кроме того, этот же код будет присутствовать во всех местах, где мы ищем управление транзакциями, что приведет к тесно связанному и не поддерживаемому коду. Декларативное управление транзакциями в Spring решает эти проблемы, используя аспектно-ориентированное программирование для достижения слабой связности и избегания шаблонного кода в нашем приложении. Давайте посмотрим, как Spring делает это на простом примере. Прежде чем мы перейдем к нашему проекту Spring, давайте настроим некоторые базы данных для нашего использования.

Управление транзакциями в Spring – Настройка базы данных

Мы создадим две таблицы для нашего использования и обновим обе из них в одной транзакции.

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;

Мы могли бы определить здесь отношение внешнего ключа от столбца id адреса к столбцу id клиента, но для простоты я не задал здесь никаких ограничений. Наша настройка базы данных готова для проекта управления транзакциями Spring, давайте создадим простой проект Spring Maven в Spring Tool Suite. Структура нашего окончательного проекта будет выглядеть как на изображении ниже. Давайте рассмотрим каждый элемент по отдельности, вместе они обеспечат простой пример управления транзакциями Spring с JDBC.

Управление транзакциями Spring – Зависимости Maven

Поскольку мы используем API JDBC, нам нужно будет включить зависимость spring-jdbc в наше приложение. Мы также нуждаемся в драйвере базы данных MySQL для подключения к базе данных MySQL, поэтому мы также включим зависимость mysql-connector-java. Артефакт spring-tx предоставляет зависимости управления транзакциями, обычно он включается автоматически STS, но если его нет, то вам нужно также его включить. Вы можете увидеть некоторые другие зависимости для ведения журнала и модульного тестирования, однако мы не будем использовать ни одну из них. Наш окончательный файл pom.xml выглядит как ниже приведенный код.

<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 – Классы модели

Мы создадим два Java Bean, Customer и Address, которые будут отображаться на наши таблицы.

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

Обратите внимание, что у бина Customer есть Address как одна из его переменных. Когда мы будем реализовывать DAO для Customer, мы получим данные как для таблицы customer, так и для address, и выполним два отдельных запроса на вставку для этих таблиц, и вот почему нам нужно управление транзакциями, чтобы избежать несогласованности данных.

Управление транзакциями в Spring – Реализация DAO

Давайте реализуем DAO для бина Customer, для простоты у нас будет только один метод для вставки записи как в таблицу customer, так и в таблицу 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");
	}

}

Обратите внимание, что реализация CustomerDAO не заботится о управлении транзакциями. Таким образом, мы добиваемся разделения обязанностей, потому что иногда мы получаем реализации DAO от третьих сторон, и мы не имеем контроля над этими классами.

Управление транзакциями в весеннем декларативном стиле – Сервис

Давайте создадим Службу Клиента, которая будет использовать реализацию CustomerDAO и обеспечивать управление транзакциями при вставке записей в таблицы клиента и адреса в один метод.

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

}

Если вы обратите внимание на реализацию CustomerManager, вы увидите, что она просто использует реализацию CustomerDAO для создания клиента, но обеспечивает декларативное управление транзакциями через аннотацию createCustomer() с @Transactional. Этого достаточно для получения преимуществ управления транзакциями в Spring. @Transactional аннотацию можно применять как к методам, так и ко всему классу. Если вы хотите, чтобы все ваши методы имели функции управления транзакциями, вы должны аннотировать свой класс этой аннотацией. Узнайте больше о аннотациях в Руководстве по аннотациям Java. Остается только провязать весенние бины, чтобы заработал пример управления транзакциями в Spring.

Управление транзакциями в Spring – Конфигурация бина

Создайте файл конфигурации Spring Bean с именем “spring.xml”. Мы будем использовать его в нашей тестовой программе для связывания бинов Spring и выполнения нашей JDBC программы для тестирования управления транзакциями.

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

Важные моменты, которые следует отметить в файле конфигурации бина Spring:

  • tx:annotation-driven используется для указания контексту Spring, что мы используем конфигурацию управления транзакциями на основе аннотаций. Атрибут transaction-manager используется для предоставления имени бина менеджера транзакций. Значение по умолчанию для transaction-manager – transactionManager, но я все равно установил его для избежания путаницы. Атрибут proxy-target-class используется для указания контексту Spring использовать прокси на основе классов; без него вы получите исключение времени выполнения с сообщением, таким как 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]
  • Поскольку мы используем JDBC, мы создаем бин transactionManager типа org.springframework.jdbc.datasource.DataSourceTransactionManager. Это очень важно, и мы должны использовать правильный класс реализации менеджера транзакций в зависимости от нашего использования API транзакций.
  • dataSource бин используется для создания объекта DataSource, и нам необходимо предоставить свойства конфигурации базы данных, такие как driverClassName, url, username и password. Измените эти значения в соответствии с вашими локальными настройками.
  • Мы внедряем dataSource в бин customerDAO. Аналогично, мы внедряем бин customerDAO в определение бина customerManager.

Наша настройка готова, давайте создадим простой класс тестирования для проверки нашей реализации управления транзакциями.

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");
		 // установка значения более 20 символов, чтобы возникло исключение SQLException 
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

Обратите внимание, что я явно устанавливаю значение столбца адреса слишком длинным, чтобы при вставке данных в таблицу Address возникло исключение. Теперь, когда мы запускаем нашу тестовую программу, мы получаем следующий вывод.

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

Обратите внимание, что сообщение в журнале говорит о том, что данные успешно вставлены в таблицу клиентов, но исключение, вызванное драйвером базы данных MySQL, явно указывает на то, что значение слишком длинное для столбца адреса. Если вы проверите таблицу Customer, то там не найдете ни одной строки, что означает полное откатывание транзакции. Если вы задаетесь вопросом, где происходит волшебство управления транзакциями, внимательно посмотрите на журналы и заметьте классы AOP и Proxy, созданные фреймворком Spring. Фреймворк Spring использует советы Around для создания прокси-класса для CustomerManagerImpl и коммитит транзакцию только в случае успешного возврата метода. Если возникает исключение, происходит полный откат транзакции. Я бы порекомендовал вам прочитать Пример Spring AOP, чтобы узнать больше о модели аспектно-ориентированного программирования. Это все, что касается примера управления транзакциями Spring. Скачайте образец проекта по следующей ссылке и поиграйтесь с ним, чтобы узнать больше.

Скачать проект управления транзакциями Spring JDBC

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