mybatis中的事务控制

mybatis中执行sql是从SqlSession开始的,SqlSession中提供了各种操作数据库的方法

SqlSession中持有执行器Executor对象,通过执行器来执行sql

mybatis事务的本质是通过connection实现的,通过connection控制事务的提交,回滚,只有通过同一个connection执行的sql才能被控制住

一、mybatis单独使用的情况

public class Test02 {

    public static void main(String[] args) throws IOException {
        InputStream resource = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource);
        //获取sqlsession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //通过sqlsession执行sql
        sqlSession.insert("com.lyy.mybatis_source.mapper.BankMapper.insert");
        //提交事务
        sqlSession.commit();
    }
}

单独使用mybatis时,一般会按上边的步骤来进行。其中openSession方法如果传true创建出的sqlsession会自动提交事务,传false或者不传得到的sqlsession需要手动调用commit方法来提交事务

SqlSessionFactory是一个接口,SqlSessionFactoryBuilder.build方法得到的是其实现类DefaultSqlSessionFactory的对象,openSession方法会得到DefaultSqlSession对象,下面来分析下

DefaultSqlSession的commit方法,

@Override
  public void commit() {
    commit(false);
  }

  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

可以看到这个commit方法最终会调用执行器executor的commit方法.执行器Executor是一个接口,常用的实现类有三个SimpleExecutor,ReuseExecutor,BatchExecutor,

sqlSessionFactory.openSession方法调用的时候可以指定执行器的类型,如果不指定创建的是SimpleExecutor

所以继续分析这个执行器的commit方法,其调用的是父类BaseExecutor中的方法

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

可以看到最后调用的是transaction.commit(),这个transaction是BaseExecutor类中的一个属性

protected Transaction transaction;

Transaction是mybatis定义的一个接口,其中提供了获取连接,操作事务等的方法,

public interface Transaction {

  Connection getConnection() throws SQLException;

  void commit() throws SQLException;

  void rollback() throws SQLException;

  void close() throws SQLException;

  Integer getTimeout() throws SQLException;

}

运行时使用哪个实现类取决于

mybatis配置文件中指定的transactionManager类型

<environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/transaction_test"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
</environments>

这个配置文件中指定的是JDBC,所以最后会使用JdbcTransaction这个实现类

继续看这个类中的commit方法

  @Override
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }

其中connection是这个类中的一个属性,代表的是执行sql时使用的数据库连接,这里通过连接对象执行commit方法来提交事务,并且如果这个连接设置的是自动提交事务这个方法就什么都不做。

总结下来就是sqlSession.commit方法最终会通过Transaction中的connection来提交事务。

sqlsession--->Executor-->Transaction-->connection

可以推断,sqlsession中执行sql时肯定也是调用这个Transaction.getConnection来获取连接,这样才能保证执行sql时和提交事务时使用的是同一个连接

JdbcTransaction中获取连接的方法

  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }

再来思考一个问题,Executor中的Transaction是什么时候赋值的?

这就需要看下DefaultSqlSessionFactory.openSession()方法,

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // configuration是DefaultSqlSessionFactory中的一个属性
      final Environment environment = configuration.getEnvironment();
      // 解析配置文件的过程中会创建出transactionFactory,并设置给environment
      // 再把environment设置到configuration对象的属性上
        final TransactionFactory transactionFactory = 
          getTransactionFactoryFromEnvironment(environment);
      // 通过transactionFactory来获取Transaction对象
      // 基于我们的配置文件这里使用的是JdbcTransactionFactory
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建 Executor对象时传入了tx对象和执行器类型
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我们mybatis配置文件中配置的事务类型是JDBC,所以这里使用的TransactionFactory是 JdbcTransactionFactory

public class JdbcTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

所以当我们使用同一个sqlSession来执行多条sql时,因为每次都是按

sqlsession--->Executor-->Transaction-->connection 这样的方式去获取数据库连接,所以第一次执行时会获取一个新连接,后续的执行都是从Transaction中拿到已有的connection执行sql,保证了使用同一个connection,

最终再通过这个connection来提交事务

二、mybatis和spring整合的情况

mybatis官方提供了一个mybatis-spring包可以用来整合spring。

整合spring后,最终的sql语句还是要通过SqlSession来执行的,也就是DefaultSqlSession,只不过为了和spring整合做了几层代理,所以从容器中获取的sqlSession是SqlSessionTemplate类型的,这个类也实现了

SqlSession接口

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    
    // 这是sqlsession的代理
    private final SqlSession sqlSessionProxy;
    
    //这是构造方法
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    
    //这是在构造方法中给sqlsession的代理赋值
    //具体逻辑是在SqlSessionInterceptor中实现,它是当前类中的内部类
    this.sqlSessionProxy = 
        (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());
  }
  
  //内部类
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在这个invoke方法中才会真正的去获取Sqlession,然后用sqlsession去执行sql
        //这里获取到的肯定也是 DefaultSqlSession
        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
        try {
            //执行方法,可以看做是对上边获取到的sqlSession对象中的方法用动态代理来做增强
            Object result = method.invoke(sqlSession, args);
            //这些就是增强逻辑
            //这个判断的意思是如果没有用spring来控制事务,就在这里使用sqlSession提交事务
            //如果spring控制事务,这里不做操作让spring统一管理事务
            if (!isSqlSessionTransactional(sqlSession, 	
                                           SqlSessionTemplate.this.sqlSessionFactory)) {
              sqlSession.commit(true);
            }
        return result;
        } catch {
            //省略
        } finally {
            //省略
        }
    }
  }
}

这个 getSqlSession方法是mybatis-spring包中提供的工具类SqlSessionUtils中的方法

SqlSessionUtils

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	// TransactionSynchronizationManager是spring提供的一个同步器,里边有许多ThreadLocal,
    // 通过它可以保证同一个线程范围内多次获取得到的是同一个sqlsession
    // 它里边以键值对的形式存数据,在这里sessionFactory就是键,
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
    //第一次访问时需要新开启一个Sqlsession然后注册到同步器中,下次访问就可以直接获取
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

上边的代码保证了调用dao方法执行sql时,同一个线程范围内使用的都是同一个sqlsession对象,

再根据第一部分的结论 sqlsession--->Executor-->Transaction 就可以保证每次获取的都是同一个transaction。

那么思考一个问题,spring怎么来控制这种情况的事务?

分析下,要控制事务,spring必须获取到connection,而从上边的分析connection被封装在Transaction中,spring如何获取到connection呢

mybatis-spring中也提供了一个Transaction的实现类,SpringManagedTransaction

这种情况下使用的是这个实现类,看下其中获取connection的方法

SpringManagedTransaction中的源码

public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
  }

同样的,只有第一次调用会新建,其余直接返回。继续看DataSourceUtils.getConnection

这是spring-jdbc提供的一个获取连接的工具类,也就是mybatis是通过spring来获取connect,这样就有机会把这个connection再暴露给spring,后边spring就可以通过这个connection来管理事务

public abstract class DataSourceUtils {
    
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            //调用这个方法去真正获取连接
            return doGetConnection(dataSource);
        } catch (SQLException var2) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
        } catch (IllegalStateException var3) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
        }
    }

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
        //可以看到是先从同步器TransactionSynchronizationManager中获取连接
        // 这里边有ThreadLocal可以存数据库连接,第一次获取创建一个connection存到同步器里
        // 下次再访问时就可以从同步器中直接取。
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
            logger.debug("Fetching JDBC Connection from DataSource");
            //进到这里表示是第一次获取连接,这个方法中会从dataSource获取连接
            Connection con = fetchConnection(dataSource);
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                try {
                    ConnectionHolder holderToUse = conHolder;
                    if (conHolder == null) {
                        holderToUse = new ConnectionHolder(con);
                    } else {
                        conHolder.setConnection(con);
                    }

                    holderToUse.requested();
                    //注册connection到同步器中,下次可以直接获取
                    TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                    holderToUse.setSynchronizedWithTransaction(true);
                    if (holderToUse != conHolder) {
                        TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                    }
                } catch (RuntimeException var4) {
                    releaseConnection(con, dataSource);
                    throw var4;
                }
            }

            return con;
        } else {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }

            return conHolder.getConnection();
        }
    }
    
}

总结下来这个方法会先从同步器TransactionSynchronizationManager中获取连接,如果获取不到就新建一个再放进去,而这个类是spring提供的,所以spring事务管理模块可以通过这个类来获取连接。

所以总结下spring+mybatis的事务管理流程,对于一个有事务的service方法,

(1) 刚开始肯定是spring先开启事务,开启事务就是获取连接,设置连接的autoCommit为false,然后会把连接跟当前线程绑定,设置到同步器TransactionSynchronizationManager

(2) mybatis执行sql时按照

sqlsession--->Executor-->Transaction-->DataSourceUtils-->TransactionSynchronizationManager

-->connection 就可以获取到spring开启事务时放进去的那个connection,然后执行sql

(3) 不管中间执行了多少sql,因为同一个线程内使用的是同一个sqlSesssion,所以

都是通过同一个connection执行的

(4) spring从TransactionSynchronizationManager中获取到connection提交或回滚事务

在补充两点,上边讲到spring+mybaits时执行sql使用的是SqlSessionTemplate,因为它实现了Sqlsession接口,所以其中也有commit方法,但直接调这个方法会抛异常,官方不让这样调用

  @Override
  public void commit() {
    throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
  }

那如果我整合了spring后直接通过DefaultSqlSession来调用commit方法会怎么样呢?

我们思考下这个commit方法最终调的是Transaction中的commit,此时实现类是SpringManagedTransaction

看下其源码

  @Override
  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
      this.connection.commit();
    }
  }

注意这里头有个isConnectionTransactional变量,当spring事务开启时,走到这个方法时这个变量会是true,所以不进if,这个方法什么都不做。还是会让spring来管理事务,调sqlsession.commit方法是无效的。

至于spring是如何进行事务管理的,这又是一大内容,后边再具体描述。简单来讲,spring的事务管理是基于aop

实现的,方法执行时实现事务功能的入口在TransactionInterceptor这个类的invoke方法中,

然后会执行到TransactionAspectSupport#createTransactionIfNecessary-->

AbstractPlatformTransactionManager#getTransaction-->startTransaction