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