Поиск по сайту:

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


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

Весеннее управление транзакциями

  1. Поддержка декларативного управления транзакциями. В этой модели Spring использует АОП вместо транзакционных методов для обеспечения целостности данных. Это предпочтительный подход, который работает в большинстве случаев.
  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;

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

Поскольку мы используем JDBC API, нам нужно будет включить в наше приложение зависимость 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>

Я обновил версии Spring до последней на сегодняшний день. Убедитесь, что драйвер базы данных MySQL совместим с вашей установкой mysql.

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 имеет адрес в качестве одной из своих переменных. Когда мы будем реализовывать DAO для клиентов, мы получим данные как для таблицы клиентов, так и для таблицы адресов, и мы выполним два отдельных запроса на вставку для этих таблиц, поэтому нам нужно управление транзакциями, чтобы избежать несогласованности данных.

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

Давайте реализуем компонент DAO for Customer. Для простоты у нас будет только один метод для вставки записи в таблицы клиентов и адресов.

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 от третьих лиц, и у нас нет контроля над этими классами.

Spring Декларативное управление транзакциями — Сервис

Давайте создадим Customer Service, который будет использовать реализацию 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. Учебник по аннотациям Java. Единственная оставшаяся часть — это подключение spring bean-компонентов, чтобы заставить работать пример 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>

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

  • Элемент tx:annotation-driven используется, чтобы сообщить контексту Spring, что мы используем конфигурацию управления транзакциями на основе аннотаций. Атрибут transaction-manager используется для предоставления имени bean-компонента менеджера транзакций. Значение по умолчанию для менеджера транзакций — transactionManager, но я все еще использую его, чтобы избежать путаницы. Атрибут proxy-target-class используется, чтобы указать контексту Spring использовать прокси на основе класса, без него вы получите исключение времени выполнения с сообщением, таким как Exception in thread \main org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean 'customerManager' должен иметь тип [com.journaldev.spring.jdbc.service.CustomerManagerImpl], но на самом деле имел тип [com.sun.proxy.$Proxy6]
  • Поскольку мы используем JDBC, мы создаем bean-компонент transactionManager типа org.springframework.jdbc.datasource.DataSourceTransactionManager. Это очень важно, и мы должны использовать правильный класс реализации диспетчера транзакций, основанный на нашем использовании API транзакций.
  • Компонент
  • dataSource используется для создания объекта DataSource, и мы должны предоставить свойства конфигурации базы данных, такие как driverClassName, URL-адрес, имя пользователя и пароль. Измените эти значения в соответствии с вашими локальными настройками.
  • Мы внедряем dataSource в bean-компонент 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");
		// setting value more than 20 chars, so that SQLException occurs
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

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

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 framework использует рекомендации Around для создания прокси-класса для CustomerManagerImpl и фиксирует транзакцию только в случае успешного завершения метода. Если есть какое-либо исключение, это просто откат всей транзакции. Я бы посоветовал вам прочитать пример Spring AOP, чтобы узнать больше о модели аспектно-ориентированного программирования. Это все, что касается примера управления транзакциями Spring, загрузите пример проекта по ссылке ниже и поэкспериментируйте с ним, чтобы узнать больше.

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