Spring事务管理示例JDBC
Spring事务管理是Spring框架中使用最广泛,最重要的功能之一。
在任何企业应用程序中,事务管理都是一项琐碎的任务。
我们已经学习了如何使用JDBC API进行事务管理。
Spring为事务管理提供了广泛的支持,并帮助开发人员将更多的精力放在业务逻辑上,而不用担心任何系统故障时数据的完整性。
Spring交易管理
使用Spring事务管理的一些好处是:
支持声明式事务管理。
在此模型中,Spring在事务方法上使用AOP来提供数据完整性。
这是首选方法,并且在大多数情况下都有效。支持大多数事务API,例如JDBC,Hibernate,JPA,JDO,JTA等。
我们需要做的就是使用适当的事务管理器实现类。
例如,用于JDBC事务管理的org.springframework.jdbc.datasource.DriverManagerDataSource和如果我们将Hibernate用作ORM工具则是org.springframework.orm.hibernate3.HibernateTransactionManager。通过使用
TransactionTemplate
或者PlatformTransactionManager
实现支持程序化事务管理。
声明式事务管理支持我们在事务管理器中需要的大多数功能,因此我们将在示例项目中使用这种方法。
Spring事务管理JDBC示例
我们将创建一个简单的Spring JDBC项目,其中我们将在单个事务中更新多个表。
仅当所有JDBC语句成功执行时,事务才应提交,否则应回滚以避免数据不一致。
如果您了解JDBC事务管理,您可能会争辩说,通过将连接的auto-commit设置为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 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>
到目前为止,我已经将Spring版本更新为最新版本。
确保MySQL数据库驱动程序与您的mysql安装兼容。
Spring交易管理–模型类
我们将创建两个Java Bean,分别是Customer和Address,它们将映射到我们的表。
package com.theitroad.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.theitroad.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; } }
请注意,客户Bean的地址之一就是地址。
当我们为客户实施DAO时,我们将同时获取客户表和地址表的数据,并且将对这些表执行两个单独的插入查询,这就是为什么我们需要事务管理以避免数据不一致的原因。
Spring交易管理– DAO实施
让我们实现DAO for Customer Bean,为简单起见,我们只有一种方法可以在客户表和地址表中插入记录。
package com.theitroad.spring.jdbc.dao; import com.theitroad.spring.jdbc.model.Customer; public interface CustomerDAO { public void create(Customer customer); }
package com.theitroad.spring.jdbc.dao; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import com.theitroad.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.theitroad.spring.jdbc.service; import com.theitroad.spring.jdbc.model.Customer; public interface CustomerManager { public void createCustomer(Customer cust); }
package com.theitroad.spring.jdbc.service; import org.springframework.transaction.annotation.Transactional; import com.theitroad.spring.jdbc.dao.CustomerDAO; import com.theitroad.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实施来创建客户,而是通过对带有@Transactional注释的createCustomer()方法进行注释来提供声明性的交易管理。
这就是我们代码中需要做的一切,以获得Spring事务管理的好处。
@Transactional注释可以应用于方法以及整个类。
如果希望所有方法都具有事务管理功能,则应使用此注释来注释您的类。
在Java Annotations Tutorial中阅读有关注释的更多信息。
剩下的唯一部分是接线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="hyman" <property name="password" value="hyman123" </bean> <bean id="customerDAO" class="com.theitroad.spring.jdbc.dao.CustomerDAOImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="customerManager" class="com.theitroad.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上下文使用基于类的代理,没有它,您将获得运行时异常,并带有诸如" main"线程中的异常之类的消息。
必须为[com.theitroad.spring.jdbc.service.CustomerManagerImpl]类型,但实际上为[com.sun.proxy。
$Proxy6]类型由于使用的是JDBC,因此我们正在创建类型为org.springframework.jdbc.datasource.DataSourceTransactionManager`的transactionManager bean。
这非常重要,我们应该根据交易API的使用情况使用适当的交易管理器实现类。dataSource bean用于创建DataSource对象,我们需要提供数据库配置属性,例如driverClassName,url,用户名和密码。
根据您的本地设置更改这些值。我们正在将dataSource注入到customerDAO bean中。
同样,我们将customerDAO bean注入到customerManager bean定义中。
我们的设置已经准备就绪,让我们创建一个简单的测试类来测试我们的交易管理实施。
package com.theitroad.spring.jdbc.main; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.theitroad.spring.jdbc.model.Address; import com.theitroad.spring.jdbc.model.Customer; import com.theitroad.spring.jdbc.service.CustomerManager; import com.theitroad.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("hyman"); Address address = new Address(); address.setId(2); address.setCountry("San Franceco"); //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.theitroad.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27) at com.theitroad.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19) at com.theitroad.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$f71441.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.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.theitroad.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIBec7ac.createCustomer(<generated>) at com.theitroad.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.doInPreparedStatement(JdbcTemplate.java:914) at org.springframework.jdbc.core.JdbcTemplate.doInPreparedStatement(JdbcTemplate.java:907) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642) ... 16 more
请注意,该日志消息指出已成功将数据插入到客户表中,但MySQL数据库驱动程序引发的异常清楚地表明该值对于地址列而言太长。
现在,如果您要检查"客户"表,那么您将在该表中找不到任何行,这意味着交易已完全回滚。
如果您想知道事务管理的神奇力量其中发生,请仔细查看日志并注意Spring框架创建的AOP和Proxy类。
Spring框架使用Around建议为CustomerManagerImpl生成代理类,并且仅在方法成功返回时才提交事务。
如果有任何例外,那就只是回滚整个交易。