前言
- Spring的事务管理,无论是基于xml还是注解实现,本质上还是实现数据库的事务管理机制,因此要注意发送SQL的连接是否为同一个,这是实现声明式事务的关键。
- 以下案例和实现基于SSM整合框架完成,不知道如何整合SSM,可以参考我之前的博客。
准备工作
第一步:添加Spring相关依赖包
spring-context:提供了Spring框架的核心功能,包括依赖注入和控制反转等。事务管理是Spring框架的核心功能之一。
spring-tx:该依赖提供了Spring事务管理的相关类和接口,用于定义事务的属性、管理事务的生命周期、处理事务的提交和回滚等。
spring-jdbc:如果需要在Spring框架中使用JDBC(Java数据库连接)进行数据库操作,需要引入该依赖。在进行数据库操作时,可以通过事务管理来保证数据的一致性和完整性。
spring-test:在进行事务管理的单元测试时,需要引入该依赖。它提供了一些测试用例的工具方法,可以方便地进行事务管理的测试。
第二步:准备相关数据库和数据库表
t_user表如下
包含字段:id,name,password,age,代表了一个用户的基本信息
新增用户失败事务回滚案例实现
在SSM中spring的配置文件和mybatis的配置文件整合到了一起,所以事务管理器的相关配置写在spring的配置文件。
spring-mybtais.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!--开启扫描 -->
<context:component-scan base-package="com.csx"/>
<!--读取jdbc配置文件-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:mybatis/jdbc.properties"/>
</bean>
<!--配置数据源,可以替换成druid数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="initialSize" value="${initialSize}"/>
<property name="maxActive" value="${maxActive}"/>
<property name="maxIdle" value="${maxIdle}"/>
<property name="minIdle" value="${minIdle}"/>
<property name="maxWait" value="${maxWait}"/>
</bean>
<!--扫描mapper-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<property name="typeAliasesPackage" value="com.csx.entity"/>
</bean>
<!--扫描dao-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="com.csx.dao"/>
</bean>
<!--事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- spring事务传播特性:
事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:
propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
<!-- <tx:method name="tranAdd*" propagation="REQUIRED"/>-->
<!-- <tx:method name="tranDel*" propagation="REQUIRED"/>-->
<!-- <tx:method name="update*" propagation="REQUIRED"/>-->
<!-- <tx:method name="get*" propagation="SUPPORTS"/>-->
<!-- <tx:method name="query*" propagation="SUPPORTS"/>-->
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.csx.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
以上是spring整合mybatis的完整配置文件。接下来我会详细介绍spring声明式的xml配置以及其含义。
配置数据源
<!--配置数据源,可以替换成druid数据源--> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> <property name="initialSize" value="${initialSize}"/> <property name="maxActive" value="${maxActive}"/> <property name="maxIdle" value="${maxIdle}"/> <property name="minIdle" value="${minIdle}"/> <property name="maxWait" value="${maxWait}"/> </bean>配置数据源(数据库连接池),是非常重要的,目的是对我们的数据库连接集中管理,保证我们后续事务管理过程中,不同的sql使用同一条数据库连接。
定义事务管理器
<!--事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
- 将事务管理器的id声明为transactionManager的目的是为了下面配置的时候少写一个属性,一会具体说明
- 事务管理器的属性就是引用我们刚才定义的数据源
配置事务管理器
<!-- spring事务传播特性: 事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为: propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。 propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。 propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。 propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。 propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。 propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作 Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> <!-- <tx:method name="tranAdd*" propagation="REQUIRED"/>--> <!-- <tx:method name="tranDel*" propagation="REQUIRED"/>--> <!-- <tx:method name="update*" propagation="REQUIRED"/>--> <!-- <tx:method name="get*" propagation="SUPPORTS"/>--> <!-- <tx:method name="query*" propagation="SUPPORTS"/>--> </tx:attributes> </tx:advice>
- 主要是为了配置事务的传播行为(即,当多个事务集中管理时,怎么控制不同的事务的执行)
- 这里的transaction-manager="transactionManager",如果我们刚才在上面定义事务管理器的id也是transactionMannger,则可以省略transaction-manager这个属性,底层自动配置对应的事务管理器,如果名称不一致,则必须配置这个属性
配置事务的AOP
spring声明式事务,本质上底层还是基于spring的aop实现的,将多条sql统一使用同一条数据库连接管理事务
<aop:config> <aop:pointcut id="txPointCut" expression="execution(* com.csx.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config>
- pointcut,定义aop的切入点表达式,事务的控制一般是在service层进行的(如果多条sql的dao方法,不是在同一个service层方法存在,则事务失效,没有被集中管理(相当于没有书写事务))
- advisor,配置切面和切入点的关系,定义事务管理管理的方法
Dao层
UserDao
public interface UserDao {
//声明式事务
int tranAddUser(User user);
int tranDelUser(int id);
}
UserDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--
mapper为映射的根节点,用来管理DAO接口
namespace指定DAO接口的完整类名,表示mapper配置文件管理哪个DAO接口(包.接口名)
mybatis会依据这个接口动态创建一个实现类去实现这个接口,而这个实现类是一个Mapper对象
-->
<mapper namespace="com.csx.dao.UserDao">
<!--声明式事务管理-->
<insert id="tranAddUser">
insert into t_user(name ,age)
values (#{name},#{age})
</insert>
<delete id="tranDelUser">
deletes from t_user where id=#{id}
</delete>
</mapper>
这里故意将delete写成deletes,目的是让删除操作发生sql异常,以便观察sql是否发生回滚
Service层
UserService
package com.csx.service;
import com.csx.entity.User;
import com.github.pagehelper.Page;
import java.util.Map;
public interface UserService {
/*事务错误演示*/
int tranAddUser(User user);
int tranDelUser(int id);
//事务正确演示
int transTest();
}
- 将两个dao层的sql操作,写在同一个service层的方法内,可以正确管理事务;
- 将两个dao层的sql操作,写在不同的service层的方法内,无法正确管理事务(不同的service层方法不属于同一个事务管理的范围)
UserServiceImpl
package com.csx.service.impl;
import com.csx.dao.UserDao;
import com.csx.entity.User;
import com.csx.service.UserService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public int tranAddUser(User user) {
return userDao.tranAddUser(user);
}
@Override
public int tranDelUser(int id) {
return userDao.tranDelUser(id);
}
@Override
public int transTest() {
User user =new User();
user.setName("曹操");
user.setAge(45);
userDao.insertUser(user);
//删除操作一定会抛出sql异常
userDao.tranDelUser(46);
return 1;
}
}
单元测试
import com.csx.entity.User;
import com.csx.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/spring-mybatis.xml"})
public class UserTest {
@Autowired
private UserService userService;
}
@Test
public void testTran(){
User user =new User();
user.setName("曹操");
user.setAge(45);
userService.tranAddUser(user);
userService.tranDelUser(46);
}
@Test
public void transTran(){
int i = userService.transTest();
}
}
testTran():一定测试失败,本质上它们不属于同一个事务,因此互不影响。
transTran():事务生效,被事务管理控制
单元测试效果演示testTran():
抛出sql异常:
数据库数据依然新增成功,事务失效
单元测试效果演示transTran():
抛出sql异常:
数据库数据没有新增,事务生效
总结
基于xml的spring声明式事务,需要注意以下几点:
- 配置切入点表达式时,注意应当切入的是service层的方法,在业务层管理不同的dao的sql,从而达到事务控制的效果。
- 将多条sql的操作,放在同一个service方法,事务才会生效,放在不同的service方法,相当于没有进行事务管理,还是一个dao的sql对应一个数据库连接。