JDBC的春季事务管理实例
春季事务管理是Spring框架中最广泛使用且重要的功能之一。事务管理在任何企业应用程序中都是一个琐碎的任务。我们已经学会了如何使用JDBC API来进行事务管理。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实现来进行编程式事务管理。
大多数我们想要的交易管理器功能都由声明式事务管理支持,因此我们将在我们的示例项目中使用这种方法。
春季事务管理JDBC示例
我们将创建一个简单的Spring JDBC项目,在其中将在单个事务中更新多个表。只有当所有JDBC语句成功执行时,事务才应该提交,否则应该回滚以避免数据不一致。如果您了解JDBC事务管理,您可能会认为我们可以通过将连接的自动提交设置为false,并根据所有语句的结果来提交或回滚事务来轻松实现。显然,我们可以做到,但这将导致大量的样板代码,仅用于事务管理。而且,相同的代码将出现在我们寻找事务管理的所有地方,导致紧密耦合和不可维护的代码。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事务管理示例。
春季事务管理 – 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安装兼容。
事务管理 – 模型类
我们将创建两个Java Bean,Customer和Address,它们将映射到我们的表。
package com.Olivia.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.Olivia.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时,我们将获取顾客和地址表的数据,并为这些表执行两个独立的插入查询,这就是为什么我们需要事务管理来避免数据不一致性的原因。
春季事务管理 – DAO 实现
让我们为客户bean实现DAO,在简单起见,我们只需要一个方法来插入记录到客户和地址表中。
package com.Olivia.spring.jdbc.dao;
import com.Olivia.spring.jdbc.model.Customer;
public interface CustomerDAO {
public void create(Customer customer);
}
package com.Olivia.spring.jdbc.dao;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import com.Olivia.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.Olivia.spring.jdbc.service;
import com.Olivia.spring.jdbc.model.Customer;
public interface CustomerManager {
public void createCustomer(Customer cust);
}
package com.Olivia.spring.jdbc.service;
import org.springframework.transaction.annotation.Transactional;
import com.Olivia.spring.jdbc.dao.CustomerDAO;
import com.Olivia.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 beans连接起来,使得Spring事务管理示例可以正常工作。
春季事务管理-Bean配置
创建一个名为”spring.xml”的Spring Bean配置文件。我们将在测试程序中使用它来连接Spring Beans并执行我们的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="scdev" />
<property name="password" value="scdev123" />
</bean>
<bean id="customerDAO" class="com.Olivia.spring.jdbc.dao.CustomerDAOImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="customerManager" class="com.Olivia.spring.jdbc.service.CustomerManagerImpl">
<property name="customerDAO" ref="customerDAO"></property>
</bean>
</beans>
春季bean配置文件中需要注意的重要点是:
- 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.Olivia.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.
我们的设置已经就绪,让我们创建一个简单的测试类来测试我们的事务管理实现。
package com.Olivia.spring.jdbc.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.Olivia.spring.jdbc.model.Address;
import com.Olivia.spring.jdbc.model.Customer;
import com.Olivia.spring.jdbc.service.CustomerManager;
import com.Olivia.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.Olivia.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
at com.Olivia.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
at com.Olivia.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.Olivia.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
at com.Olivia.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
请注意日志消息表示成功插入数据到customer表,但MySQL数据库驱动器抛出的异常明确指出地址列的值太长。现在,如果你检查Customer表,你会发现没有任何行,这意味着事务完全回滚了。如果你想知道事务管理的魔力出现在哪里,仔细查看日志,注意Spring框架创建的AOP和代理类。Spring框架使用环绕通知为CustomerManagerImpl生成代理类,并且只有当方法成功返回时才提交事务。如果有任何异常,就会回滚整个事务。我建议你阅读Spring AOP示例,以了解更多关于面向方面编程模型的知识。关于Spring事务管理示例就介绍到这里了,你可以从下面的链接下载示例项目并进行更多实践学习。
请下载Spring JDBC事务管理项目。