MyBatis SqlSession事务与批量执行正确方式(默认不生效)

  行业动态     |      2024-01-16 01:03

1. 容易误用的写法

某些情况下会使用MyBatis的SqlSessionFactory.openSession()方法获取SqlSession对象,再进行数据库操作,但默认情况下SqlSession的事务与批量执行均不生效,假如希望使用SqlSession时事务或批量执行能够生效,则需要进行额外的处理

1.1. SqlSession事务默认不生效

调用org.apache.ibatis.session.SqlSessionFactory接口的以下openSession()方法时,默认情况下,指定autoCommit参数为false,实际上不会关闭自动提交,即事务不会生效

SqlSession openSession(boolean autoCommit);
SqlSession openSession(ExecutorType execType, boolean autoCommit);

默认情况下,以下写法效果是相同的,即事务不生效:

SqlSession sqlSession = usedSqlSessionFactory.openSession(..., (autoCommit=)false);
// 两种写法效果相同
SqlSession sqlSession = usedSqlSessionFactory.openSession(..., (autoCommit=)true);

1.2. SqlSession批量执行默认不生效

调用SqlSessionFactory接口的以下openSession()方法时,默认情况下,指定execType参数为ExecutorType.BATCH,实际上sql语句不会批量执行,还是单条执行的(使用MySQL数据库)

SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);

默认情况下,以下写法效果是相同的,即sql语句不会批量执行

SqlSession sqlSession = usedSqlSessionFactory.openSession(ExecutorType.BATCH, ...);
// 两种写法效果相同
SqlSession sqlSession = usedSqlSessionFactory.openSession(ExecutorType.SIMPLE, ...);

2. SqlSession事务正确使用方式

  • 使用方式

使用SqlSessionFactory.openSession()方法获取SqlSession时,假如需要在事务中执行数据库操作,除了指定autoCommit参数为false外,还需要进行以下处理:

在定义SqlSessionFactory接口的实现类org.mybatis.spring.SqlSessionFactoryBean时,将transactionFactory字段使用org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory的实例

假如使用XML方式定义Spring相关配置,则如下所示:

<bean id="jdbcTransactionFactory" class="org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory"/>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="failFast" value="true"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations">
        <list>
            <value>classpath*:xxx/*.xmlvalue>
        list>
    property>
    <property name="transactionFactory" ref="jdbcTransactionFactory"/>
bean>

以上SqlSessionFactoryBean需要新建一个,不能使用org.mybatis.spring.mapper.MapperScannerConfigurer中使用的sqlSessionFactory

假如将MapperScannerConfigurer中使用的sqlSessionFactory的transactionFactory使用JdbcTransactionFactory的实例,会导致MyBatis中普通的Mapper数据库操作也使用事务

  • MyBatis推荐

SqlSessionFactoryBean.setTransactionFactory()方法用于设置以上transactionFactory对象

参考该方法的说明:https://mybatis.org/spring/apidocs/reference/org/mybatis/spring/SqlSessionFactoryBean.html#setTransactionFactory(org.apache.ibatis.transaction.TransactionFactory)

SqlSessionFactoryBean默认使用的TransactionFactory是SpringManagedTransactionFactory,默认的SpringManagedTransactionFactory类能很好地适用于所有的情况,强烈推荐使用默认的TransactionFactory

MyBatis不推荐使用以上修改TransactionFactory的方式

2.1. 编程式事务建议使用方式

当需要使用编程式事务执行数据库操作时,建议使用Spring事务模板TransactionTemplate

Spring事务模板TransactionTemplate不需要人工对事务进行管理,即不需要在代码中显式执行事务开启、事务提交、事务回滚等操作,更方便稳定

可参考https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-prog-template

3. SqlSession批量执行正确使用方式

使用MySQL数据库,使用SqlSessionFactory.openSession()方法获取SqlSession时,假如需要在批量执行数据库操作,除了指定execType参数为ExecutorType.BATCH外,还需要进行以下处理:

在MySQL的jdbc url中,指定rewriteBatchedStatements=true参数,使mysql-connector对SQL语句进行重写,进行批量执行,示例如下:

jdbc:mysql://1.1.1.1:3306/db_name?characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true

进行以上处理后,调用SqlSession.flushStatements()时,会批量执行当前缓存的数据库操作(调用SqlSession.flushStatements()之前的数据库写操作会被缓存)

  • 支持批量执行的情况

仅当对同一个表执行insert、replace语句,且SQL语句的结构相同时,支持批量执行

当insert、replace语句被批量执行时,insert、replace语句会被重写为以下多值形式:

insert into xxx(...) values (...),(...);
replace into xxx(...) values (...),(...);

3.1. Statement.executeBatch()与rewriteBatchedStatements

调用Statement.executeBatch()方法时,会受到rewriteBatchedStatements参数的影响

3.2. SqlSession.flushStatements()与rewriteBatchedStatements

SqlSession.flushStatements()方法中调用了Statement.executeBatch()方法,因此SqlSession.flushStatements()也会受到rewriteBatchedStatements参数的影响

3.3. MyBatis XML insert foreach与rewriteBatchedStatements

在MyBatis的XML文件的insert语句中,使用foreach方式批量执行时,MyBatis生成的SQL语句就是多值的形式,不需要重写,因此不会受到rewriteBatchedStatements参数的影响

3.4. 批量执行建议使用方式

当需要批量执行时,建议使用MyBatis的XML文件的insert语句中foreach方式,原因如上

4. 相关组件及版本

组件 版本
spring 5.3.22
mybatis 3.5.9
mybatis-spring 2.0.7
druid 1.2.10
mysql-connector-java 8.0.31

5. 细节分析

5.1. 相关概念

在使用MyBatis执行数据库操作及事务时,涉及到以下概念:

接口名或类名 说明 相关实现类或子类
org.apache.ibatis.session.SqlSessionFactory 创建SqlSession的工厂类 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory

org.apache.ibatis.session.SqlSessionManager

org.apache.ibatis.session.SqlSession 管理Sql会话,其中包含了Executor org.apache.ibatis.session.SqlSessionManager

org.apache.ibatis.session.defaults.DefaultSqlSession

org.mybatis.spring.SqlSessionTemplate

org.apache.ibatis.executor.Executor 数据库操作执行类,其中包含了Transaction org.apache.ibatis.executor.CachingExecutor

org.apache.ibatis.executor.BaseExecutor

org.apache.ibatis.executor.SimpleExecutor

org.apache.ibatis.executor.BatchExecutor

org.apache.ibatis.executor.ReuseExecutor

org.apache.ibatis.transaction.TransactionFactory 创建Transaction的工厂类 org.mybatis.spring.transaction.SpringManagedTransactionFactory

org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory

org.apache.ibatis.transaction.managed.ManagedTransactionFactory

org.apache.ibatis.transaction.Transaction 管理事务,其中包含了Connection org.apache.ibatis.transaction.managed.ManagedTransaction

org.apache.ibatis.transaction.jdbc.JdbcTransaction

org.mybatis.spring.transaction.SpringManagedTransaction

java.sql.Connection 数据库连接

5.2. 基本步骤

5.2.1. 初始化

进行初始化操作时的步骤如下:

  • SqlSessionFactory获取TransactionFactory
  • TransactionFactory创建Transaction
  • SqlSessionFactory创建Executor
  • SqlSessionFactory创建SqlSession

如下所示:

SqlSessionFactory
    -> TransactionFactory
	    -> Transaction
	-> Executor
	-> SqlSession

5.2.2. 数据库操作

进行获取数据库连接、提交/回滚事务等数据库操作时,步骤如下:

  • SqlSession调用其中的Executor
  • Executor调用其中的Transaction

如下所示:

SqlSession
    -> Executor
        -> Transaction

5.3. SqlSessionFactoryBean的openSession()方法实现

SqlSessionFactoryBean类没有直接实现SqlSessionFactory接口,而是实现了org.springframework.beans.factory.FactoryBean接口,泛型类型为SqlSessionFactory,代码如下:

public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, ...

FactoryBean接口中,getObject()方法返回工厂类管理的对象的实例,相当于做了一层代理

SqlSessionFactoryBean.getObject()方法返回的是字段SqlSessionFactory sqlSessionFactory

SqlSessionFactoryBean.afterPropertiesSet()方法中,将sqlSessionFactory字段赋值为buildSqlSessionFactory()方法返回值

SqlSessionFactoryBean.buildSqlSessionFactory()方法中,返回了sqlSessionFactoryBuilder.build()方法返回值,sqlSessionFactoryBuilder字段类型为org.apache.ibatis.session.SqlSessionFactoryBuilder

SqlSessionFactoryBuilder.build()方法中,创建了DefaultSqlSessionFactory对象并返回

SqlSessionFactoryBean类相当于继承了DefaultSqlSessionFactory类

SqlSessionFactoryBean类未实现openSession()方法,可继承DefaultSqlSessionFactory类的对应方法

5.4. SqlSessionFactory.openSession()方法实现-TransactionFactory、Transaction、Executor、SqlSession初始化过程

调用SqlSessionFactoryBean.openSession()方法时,实际调用的是DefaultSqlSessionFactory.openSession()方法

在DefaultSqlSessionFactory.openSession()方法中,会通过TransactionFactory创建Transaction,将Transaction与Executor进行关联,再将Executor与SqlSession关联

DefaultSqlSessionFactory类有多个openSession()方法,参数中不包含Connection的openSession()方法都是调用openSessionFromDataSource()方法:

若openSession()方法有指定autoCommit参数,则调用openSessionFromDataSource()方法时autoCommit参数使用指定的值;

若openSession()方法未指定autoCommit参数,则调用openSessionFromDataSource()方法时autoCommit参数使用固定值false

DefaultSqlSessionFactory.openSessionFromDataSource()方法部分代码如下:

Transaction tx = null;
try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
}

以上方法的执行步骤如下:

  • 获取环境Environment

调用org.apache.ibatis.session.Configuration.getEnvironment()方法

用于获取当前配置对应的环境信息

  • 获取事务工厂TransactionFactory

调用getTransactionFactoryFromEnvironment()方法

用于获取当前环境对应的TransactionFactory

在getTransactionFactoryFromEnvironment()方法中,若Environment.getTransactionFactory()方法返回值非null,则使用对应的TransactionFactory

对Environment中TransactionFactory进行赋值的过程见后文对SqlSessionFactoryBean.buildSqlSessionFactory()方法的分析

  • 获取事务Transaction

调用TransactionFactory.newTransaction()方法

用于创建org.apache.ibatis.transaction.Transaction

TransactionFactory存在多个实现类,newTransaction()方法有不同的实现,TransactionFactory及对应的Transaction实现类如下:

TransactionFactory实现类 Transaction实现类 对应包名
SpringManagedTransactionFactory SpringManagedTransaction org.mybatis.spring.transaction
JdbcTransactionFactory JdbcTransaction org.apache.ibatis.transaction.jdbc
ManagedTransactionFactory ManagedTransaction org.apache.ibatis.transaction.managed
  • 获取执行器Executor

调用Configuration.newExecutor(),使用Transaction、ExecutorType创建Executor

Configuration.newExecutor()方法部分代码如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }

ExecutorType类型决定了创建的Executor类型:

ExecutorType类型 对应的Executor类型 说明
BATCH BatchExecutor 支持批量执行的执行器
REUSE ReuseExecutor 支持重用预编译SQL语句的执行器
SIMPLE SimpleExecutor 简单执行器

BatchExecutor、ReuseExecutor、SimpleExecutor都是org.apache.ibatis.executor.BaseExecutor的子类

以上三种Executor的构造函数中都会调用BaseExecutor的构造函数,在其中会将参数中指定的Transaction transaction保存到类的字段中

Configuration.newExecutor()方法执行的操作如下:

使用Transaction、ExecutorType创建对应的Executor(SimpleExecutor、BatchExecutor)等

由于cacheEnabled字段默认为true,因此会使用已创建的Executor创建CachingExecutor,使用Executor delegate字段保存刚创建的Executor

  • 获取SQL会话SqlSession

使用Executor、autoCommit创建DefaultSqlSession

DefaultSqlSessionFactory.openSessionFromDataSource()方法返回的类型为DefaultSqlSession

5.5. BaseExecutor中通过Transaction获取Connection的过程

在BaseExecutor的子类BatchExecutor、ReuseExecutor、SimpleExecutor,数据库读写操作对应的doQuery()、doQueryCursor()、doUpdate()方法中,都会先直接或间接调用getConnection()方法,以获取数据库连接Connection

在BaseExecutor.getConnection()方法中,会调用Transaction transaction字段的getConnection()方法并返回:

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
        return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
        return connection;
    }
}

5.6. SqlSession.commit()方法执行过程

5.6.1. SqlSession的commit过程

SqlSession.commit()方法对应DefaultSqlSession.commit()方法,该方法中会调用commit(boolean force)方法,参数值为false:

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

@Override
public void commit(boolean force) {
    try {
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;

在commit(boolean force)方法中,会调用Executor executor字段的commit()方法,参数值为isCommitOrRollbackRequired()方法的返回值

isCommitOrRollbackRequired()方法代码如下:

private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
}

由于force参数为false,因此仅当autoCommit=false且dirty=true时,isCommitOrRollbackRequired()方法会返回true

即仅当关闭自动提交,且执行了写操作时,SqlSession在调用Executor的commit()方法时,传入参数为true

在DefaultSqlSession.update()方法中,会将dirty字段设置为true

insert()、delete()方法也是调用了update()方法

即dirty字段代表当前Sql会话是否有执行数据库写操作

5.6.2. Executor的commit过程

在SqlSession.commit()方法中调用Executor.commit()方法时,步骤如下:

  • CachingExecutor.commit()

在DefaultSqlSession.commit()方法中,由于DefaultSqlSession中的Executor executor字段类型为CachingExecutor,因此会调用CachingExecutor.commit()方法

在CachingExecutor.commit()方法中,会调用Executor delegate字段的commit()方法

delegate字段的类型为BaseExecutor的子类BatchExecutor、ReuseExecutor、SimpleExecutor,由于以上类中没有实现commit()方法,因此会调用父类BaseExecutor的commit()方法

  • BaseExecutor.commit()

在BaseExecutor.commit()方法中,当参数required=true时会调用Transaction transaction字段的commit()方法,参数required对应DefaultSqlSession.commit()方法中isCommitOrRollbackRequired()方法的返回值

即DefaultSqlSession中的autoCommit=false(关闭自动提交)且dirty=true(执行了写操作)时,BaseExecutor.commit()方法中会执行事务对象对应的Transaction.commit()方法

5.6.3. Transaction的commit过程

Transaction实现类的commit()方法处理见后续内容

5.6.4. SqlSession.commit()方法执行过程调用堆栈

以默认使用的Transaction实现类SpringManagedTransaction为例,SqlSession.commit()方法执行过程调用堆栈如下:

org.apache.ibatis.session.defaults.DefaultSqlSession.commit(DefaultSqlSession:214)
org.apache.ibatis.session.defaults.DefaultSqlSession.commit(DefaultSqlSession:220)
org.apache.ibatis.executor.CachingExecutor.commit(CachingExecutor:119)
org.apache.ibatis.executor.BaseExecutor.commit(BaseExecutor:244)
org.mybatis.spring.transaction.SpringManagedTransaction.commit(SpringManagedTransaction:93)

5.7. SqlSession.rollback()方法执行过程

与commit()方法执行过程类似,略

5.8. 为什么openSession(autoCommit=false)时事务默认未生效

使用SqlSessionFactory接口的实现类SqlSessionFactoryBean时,在buildSqlSessionFactory()方法中决定使用的TransactionFactory transactionFactory字段的类型,若未指定则默认使用org.mybatis.spring.transaction.SpringManagedTransactionFactory类的实例

targetConfiguration.setEnvironment(new Environment(this.environment,
    this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
    this.dataSource));

在SpringManagedTransactionFactory的newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit)方法中,生成了SpringManagedTransaction类实例并返回,在生成SpringManagedTransaction类实例时,未使用autoCommit参数

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

前文已分析,Executor执行数据库读写操作前,会先调用Transaction.getConnection()方法获取数据库连接,默认情况下会执行SpringManagedTransaction.getConnection()方法

SpringManagedTransaction.getConnection()方法中会调用SpringManagedTransaction.openConnection()方法

SpringManagedTransaction.openConnection()方法用于打开数据库连接,autoCommit字段使用当前获取的connection对象的autoCommit:

this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();

前文已分析,SqlSession.commit()方法执行时,会通过Executor,执行Transaction的commit()方法,默认情况下会执行SpringManagedTransaction.commit()方法

SpringManagedTransaction类用于进行事务提交与回滚处理的commit()、rollback()方法中,当autoCommit字段为true时,不会执行connection的commit()、rollback()方法以进行事务提交或回滚:

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

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

默认情况下使用的SpringManagedTransactionFactory对应的SpringManagedTransaction不会改变连接对象connection的autoCommit值;在MySQL中,autoCommit参数默认值为true,因此SqlSession默认情况下事务不生效

5.9. 为什么JdbcTransactionFactory能够使事务生效

  • JdbcTransactionFactory.newTransaction()方法创建事务对象过程

在TransactionFactory的实现类JdbcTransactionFactory中,newTransaction()方法(DefaultSqlSessionFactory.openSessionFromDataSource()方法中执行的)代码如下:

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

以上JdbcTransactionFactory.newTransaction()方法中会创建JdbcTransaction对象,传递了autoCommit参数,对应DefaultSqlSessionFactory.openSessionFromDataSource()方法的autoCommit参数

  • JdbcTransaction.getConnection()方法获取数据库连接过程

前文已分析,BaseExecutor中会通过Transaction.getConnection()方法获取Connection

在JdbcTransaction.getConnection()方法中,会调用openConnection()方法

在JdbcTransaction.openConnection()方法中,会调用setDesiredAutoCommit()方法,参数值为类的autoCommit字段

在setDesiredAutoCommit()方法中,会将Connection的autoCommit设置为参数desiredAutoCommit指定的值,当数据库连接的自动提交关闭时,事务可以生效

protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
        if (connection.getAutoCommit() != desiredAutoCommit) {
        if (log.isDebugEnabled()) {
            log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(desiredAutoCommit);
        }
    } 

以上调用Connection.setAutoCommit()方法设置autoCommit的参数desiredAutoCommit,对应DefaultSqlSessionFactory.openSessionFromDataSource()方法的autoCommit参数

假如使用JdbcTransactionFactory,且调用DefaultSqlSessionFactory.openSessionFromDataSource()方法时autoCommit参数值为false,则获取数据库连接时会关闭自动提交,因此事务可以生效

5.10. MyBatis Mapper初始化过程

MyBatis Mapper对应的Spring Bean的初始化过程中涉及到以下类:

org.mybatis.spring.mapper.MapperFactoryBean
org.mybatis.spring.support.SqlSessionDaoSupport
org.mybatis.spring.SqlSessionTemplate
org.apache.ibatis.binding.MapperProxy

MyBatis Mapper初始化过程比较复杂,这里不展开说明

MyBatis Mapper在完成初始化之后,会变成动态代理类,对应org.apache.ibatis.binding.MapperProxy类

MapperProxy类中的SqlSession sqlSession字段类型为org.mybatis.spring.SqlSessionTemplate

SqlSessionTemplate类中的SqlSessionFactory sqlSessionFactory字段,对应org.mybatis.spring.mapper.MapperScannerConfigurer中的SqlSessionFactory sqlSessionFactory字段(可以通过sqlSessionFactoryBeanName指定)

前文已说明,在SqlSessionFactory进行处理后,会将Transaction与SqlSession关联起来,在获取数据库连接、执行提交/回滚操作时,都需要调用Transaction

5.11. 直接使用MyBatis Mapper时获取Connection过程

直接使用MyBatis Mapper执行数据库操作时,获取数据库连接Connection的调用堆栈如下:

以下使用的是默认的SpringManagedTransactionFactory对应的SpringManagedTransaction,下同

com.sun.proxy.$Proxy38.insert(Unknown Source)
org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy:86)
org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy:145)
org.apache.ibatis.binding.MapperMethod.execute(MapperMethod:62)
org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate:272)
com.sun.proxy.$Proxy36.insert(Unknown Source)
org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate:427)
java.lang.reflect.Method.invoke(Method:498)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl:43)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl:62)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession:181)
org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession:194)
org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor:76)
org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor:117)
org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor:49)
org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor:86)
org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor:337)
org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction:66)

直接使用MyBatis Mapper执行数据库操作时,获取数据库连接Connection的过程与使用SqlSessionFactory.openSession()方法时的过程类似

5.12. 直接使用MyBatis Mapper时创建SqlSession与commit过程

直接使用MyBatis Mapper执行数据库操作时,执行commit的调用堆栈如下:

com.sun.proxy.$Proxy38.insert(Unknown Source)
org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy:86)
org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy:145)
org.apache.ibatis.binding.MapperMethod.execute(MapperMethod:62)
org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate:272)
com.sun.proxy.$Proxy36.insert(Unknown Source)
org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate:431)
org.apache.ibatis.session.defaults.DefaultSqlSession.commit(DefaultSqlSession:220)
org.apache.ibatis.executor.CachingExecutor.commit(CachingExecutor:119)
org.apache.ibatis.executor.BaseExecutor.commit(BaseExecutor:244)
org.mybatis.spring.transaction.SpringManagedTransaction.commit(SpringManagedTransaction:93)

在SqlSessionTemplate$SqlSessionInterceptor.invoke()方法中,完成获取SqlSession、执行数据库语句、提交事务等操作,代码如下:

SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
    SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
    Object result = method.invoke(sqlSession, args);
    if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
    }
    return result;
}
  • 获取SqlSession

执行SqlSessionUtils类的getSqlSession()方法,获取SqlSession

  • 执行数据库语句

执行method.invoke()方法,执行数据库语句

  • 提交事务

当isSqlSessionTransactional()返回false,即当前事务不是由Spring管理时,执行sqlSession.commit()方法,提交事务

5.13. 为什么JdbcTransactionFactory会使所有Mapper都使用事务

以上执行的SqlSessionUtils.getSqlSession()方法代码如下:

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

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

LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);

TransactionSynchronizationManager.getResource()方法用于获取Spring管理的事务信息,由于未使用Spring管理事务,因此以上获取到的SqlSessionHolder holder、SqlSession session均为null

后续会执行sessionFactory.openSession()方法,对应DefaultSqlSessionFactory的openSession(ExecutorType execType)方法:

public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
}

以上对应的DefaultSqlSessionFactory.openSession()方法中调用openSessionFromDataSource()方法时,参数3 autoCommit指定的值是false

假如SqlSessionFactoryBean中的transactionFactory字段使用JdbcTransactionFactory代替默认的SpringManagedTransactionFactory,在前文已说明在创建数据库连接时会关闭自动提交,会使事务生效

5.14. 为什么事务未提交时SqlSession关闭会回滚事务

SqlSession的数据库操作执行完毕进行关闭时,调用堆栈如下:

org.apache.ibatis.session.defaults.DefaultSqlSession.close(DefaultSqlSession:260)
org.apache.ibatis.executor.CachingExecutor.close(CachingExecutor:64)
org.apache.ibatis.executor.BaseExecutor.close(BaseExecutor:87)
org.apache.ibatis.executor.BaseExecutor.rollback(BaseExecutor:256)
org.apache.ibatis.transaction.jdbc.JdbcTransaction.rollback(JdbcTransaction:78)

DefaultSqlSession.close()部分代码如下:

executor.close(isCommitOrRollbackRequired(false));

isCommitOrRollbackRequired()方法前文已分析过,仅当关闭自动提交(autoCommit=false),且执行了写操作(dirty=true)时会返回true

BaseExecutor.close()方法中会调用rollback()方法,部分代码如下:

@Override
public void close(boolean forceRollback) {
    try {
        try {
            rollback(forceRollback);

BaseExecutor.rollback()方法代码如下:

public void rollback(boolean required) throws SQLException {
    if (!closed) {
        try {
            clearLocalCache();
            flushStatements(true);
        } finally {
            if (required) {
                transaction.rollback();
            }
        }
    }
}

在BaseExecutor.rollback()方法中,当required参数为true时,会调用Transaction transaction字段的rollback()方法。required参数对应BaseExecutor.close()方法的forceRollback参数,对应DefaultSqlSession.isCommitOrRollbackRequired()方法返回值

即DefaultSqlSession中的autoCommit=false(关闭自动提交)且dirty=true(执行了写操作)时,BaseExecutor.rollback()方法中会执行事务对象对应的Transaction.rollback()方法

(SqlSession.close()方法中会执行事务回滚操作,可以通过try-with-resource方式使用SqlSession对象,不需要在异常时显式执行rollback()方法。但由于MyBatis不建议以上使用事务的方式,因此可以忽略)

5.15. 为什么事务正常提交后SqlSession关闭不会回滚事务

在DefaultSqlSession.commit()方法中,执行Executor executor字段的commit()方法后,会将dirty字段修改为false

在以上情况下isCommitOrRollbackRequired()方法会返回false,BaseExecutor.rollback()方法执行时required参数为false,不会再执行Transaction transaction字段的rollback()方法

因此事务正常提交后SqlSession关闭不会回滚事务

5.16. 为什么openSession(ExecutorType.BATCH)时批量执行默认未生效(MySQL)

参考https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html

mysql-connector的文档中说明,rewriteBatchedStatements参数决定executeBatch()方法执行时,是否需要将预编译的insert与replace语句重写为多值语句

该参数默认值为false,默认情况下不会将SQL语句重写为批量执行形式,即默认未开启SQL语句的批量执行

5.17. 为什么只有insert、replace支持批量执行

以上mysql-connector的文档中已说明insert与replace语句支持批量执行

在mysql-connector的com.mysql.cj.QueryInfo类,构造函数QueryInfo(String sql, Session session, String encoding)中,仅当处理insert或replace语句,且rewriteBatchedStatements参数值为true时,isRewritableWithMultiValuesClause字段值为true:

boolean rewriteBatchedStatements = session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue();

...

// Only INSERT and REPLACE statements support multi-values clause rewriting.
boolean isInsert = strInspector.matchesIgnoreCase(INSERT_STATEMENT) != -1;
if (isInsert) {
    strInspector.incrementPosition(INSERT_STATEMENT.length()); // Advance to the end of "INSERT".
}
boolean isReplace = !isInsert && strInspector.matchesIgnoreCase(REPLACE_STATEMENT) != -1;
if (isReplace) {
    strInspector.incrementPosition(REPLACE_STATEMENT.length()); // Advance to the end of "REPLACE".
}

// Check if the statement has potential to be rewritten as a multi-values clause statement, i.e., if it is an INSERT or REPLACE statement and
// 'rewriteBatchedStatements' is enabled.
boolean rewritableAsMultiValues = (isInsert || isReplace) && rewriteBatchedStatements;

...

this.isRewritableWithMultiValuesClause = rewritableAsMultiValues;

QueryInfo.isRewritableWithMultiValuesClause()方法返回了isRewritableWithMultiValuesClause字段值:

public boolean isRewritableWithMultiValuesClause() {
    return this.isRewritableWithMultiValuesClause;
}

在com.mysql.cj.jdbc.ClientPreparedStatement类的executeBatchInternal()方法中,当QueryInfo.isRewritableWithMultiValuesClause()方法返回true时,才会执行executeBatchWithMultiValuesClause()方法,executeBatchWithMultiValuesClause()方法用于将预编译的insert与replace语句重写为多值语句:

if (getQueryInfo().isRewritableWithMultiValuesClause()) {
    return executeBatchWithMultiValuesClause(batchTimeout);
}

6. 可参考的内容

关于autoCommit参数及MySQL数据库操作使用事务时的执行过程,可参考以下内容:

“MySQL SQL语句与事务执行及日志分析”https://blog.csdn.net/a82514921/article/details/126563449

“Spring、MyBatis、Druid、MySQL使用事务执行SQL语句分析”https://blog.csdn.net/a82514921/article/details/126563542

关于对MySQL的SQL语句与事务执行过程的分析与监控方式,可参考以下内容:

“Spring、MyBatis、Druid、MySQL执行SQL语句与事务监控”https://blog.csdn.net/a82514921/article/details/126563558

“tcpdump、Wireshark抓包分析MySQL SQL语句与事务执行”https://blog.csdn.net/a82514921/article/details/126563471