开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情
上篇文章我们讲解了 Mybatis的 JdbcTransaction的事务及操作,演示了如何 使用jdbcTransaction来 commit, rollback Mysql操作 今天我们讲下ManagerTransaction 就是默认什么都不处理, 把事情交给容器去处理的 mybatis事务
这里我们讲的容器 就是spring容器
1.Mybatis结合Spring事务
首先要明确:如果 Spring 管理 Mybatis 的事务,那么他们俩运行时必须在同一个 Connection 的同一事务下。 明白这一点就比较容易了。接下来就是想方设法的让他们在同一个连接的同一事务下。 Spring使用了动态代理。
Mybatis 中最主要的几个类, 这几个类控制了Mybatis的 dataSource的基本配置:
- SqlSession
- SqlSessionFactory
- SqlSessionFactoryBuilder
- Executor
- Transaction
Mybatis-Spring 自动配置类, 来控制Spring与Mybatis结合 :
- SqlSessionTemplate
- MybatisAutoConfiguration
- SqlSessionFactoryBean
- MapperProxy
- MapperProxyFactory
大致流程
- SqlSessionTemplate 有属性 sqlSessionProxy即SqlSession, 创建一个JDK动态代理 InvocationHandler,实现了一个invoke方法
- 所有的Mapper的 method方法 都会掉这个invoke方法
- 先获取SqlSession, 从sqlSessionHolder中获取 sqlSession, 如果没有就创建
- 创建的时候 会走Configuration Environment中的 TransactionFactory, 默认就是 SpringManagedTransactionFactory
- 包装好SqlSessionHolder,然后向TransactionSynchronizationManager中绑定资源和注册TransactionSynchronization
- springboot中构建SqlSessionFactory时,设置了事务工厂为SpringManagedTransactionFactory
- 把TransactionSynchronization注册到了TransactionSynchronizationManager类的synchronizations静态属性中
2. 配置Spring 事务
2.1 配置DataSource 配置类
package com.jzj.tdmybatis.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.io.IOException;
@Configuration
public class DataSourceConfiguration {
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean
public DataSource dataSource(){
return new DruidDataSource();
}
@Bean
public SqlSessionFactoryBean sqlSession() {
SqlSessionFactoryBean sqlSession = new SqlSessionFactoryBean();
sqlSession.setDataSource(dataSource());
try {
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:sqlmapper/*.xml");
sqlSession.setMapperLocations(resources);
//配置自定义的Interceptro作为MyBatis的Interceptor,完成分页操作
DefinedPageInterceptor definedPageInterceptor = new DefinedPageInterceptor();
//这里的 interceptor 要用 mybatis包中的
sqlSession.setPlugins(new Interceptor[]{definedPageInterceptor});
return sqlSession;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Bean
public PlatformTransactionManager annotationDrivenTransactionManager(@Qualifier(value = "dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
自定义拦截器信息
package com.jzj.tdmybatis.config;
import cn.hutool.db.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
/**
* 利用MyBatis拦截器进行分页
*
* @Intercepts 说明是一个拦截器
* @Signature 拦截器的签名
* type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
* method 拦截的方法
* args 参数,高版本需要加个Integer.class参数,不然会报错
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Slf4j
public class DefinedPageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取StatementHandler,默认的是RoutingStatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//获取StatementHandler的包装类
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
//分隔代理对象
while (metaObject.hasGetter("h")) {
Object obj = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(obj);
}
while (metaObject.hasGetter("target")) {
Object obj = metaObject.getValue("target");
metaObject = SystemMetaObject.forObject(obj);
}
//获取查看接口映射的相关信息
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String mapId = mappedStatement.getId();
//拦截以ByInterceptor结尾的请求,统一实现分页
if (mapId.matches(".+ByInterceptor$")) {
log.info("已触发分页拦截器");
//获取进行数据库操作时管理参数的Handler
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
//获取请求时的参数
Page info = (Page) parameterHandler.getParameterObject();
//获取原始SQL语句
String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
//构建分页功能的SQL语句 底层还是limit 分页
String sql = originalSql.trim() + " limit " + info.getStartPosition() + ", " + info.getPageSize();
metaObject.setValue("delegate.boundSql.sql", sql);
}
//调用原对象方法,进入责任链下一级
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
//生成Object对象的动态代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//如果分页每页数量是统一的,可以在这里进行统一配置,也就无需再传入PageInfo信息了
}
}
2.2 配置Springboot启动类
启动类一定要加 EnableTransactionManagement 来开启事务
@SpringBootApplication
@MapperScan(basePackages="com.jzj.tdmybatis.repository.mapper")
@EnableTransactionManagement
public class TdmybatisApplication {
public static void main(String[] args) {
SpringApplication.run(TdmybatisApplication.class, args);
}
}
2.3 测试类
没有事务注解的测试12, 注意该方法是没有配置 @Transactional 事务注解的, 所以就不存在回滚操作, 不管是否有异常, DB插入数据是正常执行的
@RequestMapping("/temp/query12")
@ResponseBody
public void query12() {
UserInfoPO userInfoPO = new UserInfoPO();
userInfoPO.setUserId("aaa1");
userInfoPO.setUserName("aaa2");
userInfoPO.setAddress("aaa3");
userInfoPO.setGoods("{"deptId": 1, "deptName": "部门1", "deptLeaderId": 4}");
userInfoPO.setOrderIds("[4, 5, 6]");
userInfoPO.setAge(10);
userInfoPO.setIsDel(1);
userInfoPO.setIsDel2(1);
userInfoPO.setAddtime(1L);
userInfoPO.setModtime(1L);
userService.insert(userInfoPO);
throw new RuntimeException("异常");
}
配置 事务注解 @Transactional, 异常回滚操作, 一旦遇到异常就会回滚SQL, 插入不成功
@RequestMapping("/temp/query13")
@ResponseBody
@Transactional
public void query13() {
UserInfoPO userInfoPO = new UserInfoPO();
userInfoPO.setUserId("bbb1");
userInfoPO.setUserName("bbb2");
userInfoPO.setAddress("bbb3");
userInfoPO.setGoods("{"deptId": 1, "deptName": "部门1", "deptLeaderId": 4}");
userInfoPO.setOrderIds("[4, 5, 6]");
userInfoPO.setAge(20);
userInfoPO.setIsDel(1);
userInfoPO.setIsDel2(1);
userInfoPO.setAddtime(1L);
userInfoPO.setModtime(1L);
userService.insert(userInfoPO);
throw new RuntimeException("异常");
}
4.执行结果-无事务
执行 >curl 127.0.0.1:8800/temp/query12
出现异常, 没有事务,数据正常插入
{"timestamp":"2023-02-13T16:14:48.821+00:00",
"status":500,
"error":"Internal Server Error",
"path":"/temp/query12"}
执行日志
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3cec4b8c] will not be managed by Spring
==> Executing: SELECT LAST_INSERT_ID()
<== Columns: LAST_INSERT_ID()
<== Row: 22
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6112cd96]
2023-02-14 00:14:48.820 ERROR 20556 --- [nio-8800-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 异常] with root cause
java.lang.RuntimeException: 异常
at com.jzj.tdmybatis.controller.TestController.query12(TestController.java:265) ~[classes/:na]
执行结果
<== Row: 22
插入 一行 22 的数据
5.配置事务@Transactional
执行 >curl 127.0.0.1:8800/temp/query13
出现异常, 配置事务,数据插入失败 执行日志
# 为sql会话注册事务同步
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f954b07]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@156d2077] will be managed by Spring
==> Executing: SELECT LAST_INSERT_ID()
<== Columns: LAST_INSERT_ID()
<== Row: 23
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f954b07]
# 完成 取消事务
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f954b07]
#关闭链接SqlSession
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f954b07]
2023-02-14 00:18:25.544 ERROR 20556 --- [nio-8800-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 异常] with root cause
java.lang.RuntimeException: 异常
at com.jzj.tdmybatis.controller.TestController.query13(TestController.java:287) ~[classes/:na]
执行结果 <== Row: 23 <== Total: 1 但是 查看 DB数据, 数据并没有插入, 因为遇到异常, 事务回滚,导致数据未插入
事务生效, 抛出异常, DB数据没有正常插入,被回滚