Spring 事务管理示例 JDBC

Spring 事务管理 是 Spring 框架中最广泛使用且重要的功能之一。在任何企业应用程序中,事务管理都是一项琐碎的任务。我们已经学会如何使用 JDBC API 进行事务管理。Spring 为事务管理提供了广泛的支持,帮助开发人员更专注于业务逻辑,而不必担心在系统故障的情况下数据的完整性。

Spring 事务管理

使用 Spring 事务管理的一些好处包括:

  1. 支持声明式事务管理。在这种模型中,Spring 使用 AOP(面向切面编程)覆盖事务性方法,以提供数据完整性。这是首选的方法,在大多数情况下都能正常工作。
  2. 大多数事务API的支持,比如JDBC、Hibernate、JPA、JDO、JTA等等。我们所需做的就是使用适当的事务管理器实现类。例如,对于JDBC事务管理,我们可以使用org.springframework.jdbc.datasource.DriverManagerDataSource,而如果我们使用Hibernate作为ORM工具,则可以使用org.springframework.orm.hibernate3.HibernateTransactionManager
  3. 通过使用TransactionTemplatePlatformTransactionManager实现,支持编程式事务管理。

大多数我们希望在事务管理器中拥有的功能都得到了声明式事务管理的支持,因此我们会在示例项目中采用这种方法。

Spring事务管理JDBC示例

我们将创建一个简单的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;

我们可以在这里从地址(Address)id列定义到客户(Customer)id列的外键关系,但为了简单起见,我在这里没有定义任何约束。我们的数据库设置已准备好用于Spring事务管理项目,让我们在Spring Tool Suite中创建一个简单的Spring Maven项目。我们的最终项目结构将如下图所示。 让我们逐个查看每个部分,它们将共同提供一个带有JDBC的简单Spring事务管理示例。

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>

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 Bean 将 Address 作为其变量之一。当我们为 Customer 实现 DAO 时,我们将获取客户和地址表的数据,并针对这两个表执行两个单独的插入查询,这就是为什么我们需要事务管理以避免数据不一致的原因。

Spring 事务管理 – DAO 实现

让我们为 Customer Bean 实现 DAO,为简单起见,我们将只有一个方法来在客户和地址表中插入记录。

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声明式事务管理 – 服务

让我们创建一个客户服务,该服务将使用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 Bean连接起来,以使Spring事务管理示例正常工作。

Spring事务管理 – Bean配置

创建一个名为“spring.xml”的Spring Bean配置文件。我们将在测试程序中使用它来连接Spring Bean并执行我们的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 Bean配置文件中需要注意的重点是:

  • tx:annotation-driven 元素用于告诉Spring上下文,我们正在使用基于注解的事务管理配置。 transaction-manager 属性用于提供事务管理器bean的名称。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,我们正在创建类型为 org.springframework.jdbc.datasource.DataSourceTransactionManager 的 transactionManager bean。这非常重要,我们应该根据我们的事务API使用选择合适的事务管理器实现类。
  • dataSource bean 用于创建 DataSource 对象,我们需要提供数据库配置属性,如 driverClassName、url、username 和 password。根据您的本地设置更改这些值。
  • 我们正在向 customerDAO bean 中注入 dataSource。同样,我们将 customerDAO bean 注入到 customerManager bean 的定义中。

我们的设置已经准备好,让我们创建一个简单的测试类来测试我们的事务管理实现。

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

}

请注意,我明确设置地址列值过长,以便在插入数据到地址表时发生异常。现在当我们运行我们的测试程序时,我们会得到以下输出。

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数据库驱动程序抛出的异常明确表示地址列的值过长。现在,如果您检查客户表,您将发现没有任何行,这意味着事务已完全回滚。如果您想知道事务管理的魔术发生在哪里,请仔细查看日志,并注意Spring框架创建的AOP和代理类。Spring框架使用环绕建议为CustomerManagerImpl生成代理类,仅在方法成功返回时提交事务。如果有任何异常,它只是回滚整个事务。我建议您阅读《Spring AOP示例》以了解更多关于面向方面编程模型的信息。关于Spring事务管理示例就介绍到这里,您可以从下面的链接下载示例项目并进行更多实践。

下载Spring JDBC事务管理项目

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