准备系列-Mybatis(十四) TKmybatis 集成SpringBoot事务配置

301 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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

大致流程

  1. SqlSessionTemplate 有属性 sqlSessionProxy即SqlSession, 创建一个JDK动态代理 InvocationHandler,实现了一个invoke方法
  2. 所有的Mapper的 method方法 都会掉这个invoke方法
  3. 先获取SqlSession, 从sqlSessionHolder中获取 sqlSession, 如果没有就创建
  4. 创建的时候 会走Configuration Environment中的 TransactionFactory, 默认就是 SpringManagedTransactionFactory
  5. 包装好SqlSessionHolder,然后向TransactionSynchronizationManager中绑定资源和注册TransactionSynchronization
  6. springboot中构建SqlSessionFactory时,设置了事务工厂为SpringManagedTransactionFactory
  7. 把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 的数据 image.png

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数据, 数据并没有插入, 因为遇到异常, 事务回滚,导致数据未插入

image.png

事务生效, 抛出异常, DB数据没有正常插入,被回滚