Executor
在MyBatis
中有关Executor
的配置如下:
设置名 | 描述 | 有效值 | 默认值 |
---|---|---|---|
defaultExecutorType |
配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
也就是说,在MyBatis
中有三种Executor
:
SimpleExecutor
: 就是普通的执行器。-
ReuseExecutor
: 执行器会重用预处理语句(PreparedStatement
) -
BatchExecutor
: 批量执行器,底层调用JDBC
中Statement#batch()
Executor
的作用?
首先看SqlSession
的一个查询方法的源代码:
DefaultSqlSession#selectList()
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds){
//获取对应的配置
MappedStatement ms = configuration.getMappedStatement(statement);
//调用`Executor`的查询
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
从上面的源码我们可以看出来,Executor
实际上就是一个打工仔。所有SqlSession
执行的方法,基本上都是委托给Executor
执行的。
那以上3中执行器代码中有什么不同呢?
在MyBatis
中,如果开启了二级缓存,那么会使用MyBatis
的二级缓存,而这个二级缓存的作用点,便在于Executor
的其中一个。
换一种意思便是,在MyBatis
中,Executor
的实现一共有4个,另外一个为CachingExecutor
CacheingExecutor
作为其他3个Executor
的装饰者,本身维护了一套缓存机制,底层的调用依然是使用的其他3个Executor
,因此这里暂时先跳过;
其他3个Executor
都继承自BaseExecutor
,BaseExecutor
中实现了一些通用的方法,
在Executor
中,所有的SQL
都被归为两类 : 修改(增,删,改) 和 查询 (查),因此Executor
中将所有的SQL
操作都归为了两个方法
BaseExecutor
BaseExecutor
很像是设计模式中的模板方法模式,它实现了Executor
的一些通用方法,比如正常的逻辑判断,一级缓存,日志等,然后在真正需要调用逻辑的时候再调用abstract
方法,比如:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
//添加日志信息
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
//逻辑判断
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空一级缓存
clearLocalCache();
//获取结果
return doUpdate(ms, parameter);
}
其中
doUpdate()
便是需要子类自己实现
SimpleExecutor
SimpleExecutor
是一个最简单的执行器,没有做额外的操作。
首先看Executor
的update()
方法
SimpleExecutor#doUpdate()
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
//获取配置
Configuration configuration = ms.getConfiguration();
//创建`StatementHandler`
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
//初始化statement
stmt = prepareStatement(handler, ms.getStatementLog());
//执行具体的方法
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
可以看到这里大概就是获取配置,创建StatementHandler
,初始化Statement
,然后获取结果。
几乎所有的操作都交给了StatementHandler
操作了,为什么还有分一层Executor
呢?
答案在于prepareStatement
和调用handler
的方法。
前面说过3种Executor
的不同功能,对于ReuseExecutor
会重用Statement
,对于BatchExecutor
会执行批处理。
下面看看体现这些功能的代码:
ReuseExecutor#prepareStatement()
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取处理SQL
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
//查看缓存中时候已经有statement
//如果存在,则直接使用
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
//如果不存在,则新建statement,并缓存起来
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
BatchExecutor
BatchExecutor
是在我们需要循环/批量执行某些操作的时候,可以进行试用,其底层使用的statment#addBatch()
方法,一般对于mysql
,要想此方法发挥其性能的优点,需要在连接的时候添加以下两个命令:
useServerPrepStmts=false
rewriteBatchedStatements=true
BatchExecutor#doUpdate()
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
//判断是和上一次执行SQL是同一条SQL
//并且statment是否也相同
//如果相同,则继续执行addBatch()
//否则进行新建
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
//获取最后一个statement的坐标
//为什么不用栈???
int last = statementList.size() - 1;
//获取最后一次添加的statement
stmt = statementList.get(last);
//设置statement超时时间
//取事务和statement设置的较小值
applyTransactionTimeout(stmt);
//设置参数
handler.parameterize(stmt);
//保存参数,后续其他业务逻辑需要,比如KeyGenerator
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
//否则,新建一个statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); //fix Issues 322
currentSql = sql;
currentStatement = ms;
//每新建一个statement,都将其放在List中
//方便后续处理
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
//调用statement 的batch方法
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
可能有人不大理解这段代码。先贴出
JDBC
的batch
使用方式:
//插入1000条测试代码
PreparedStatement psts = conn.prepareStatement(sql);
for(int i=0;i<1000;i++){
psts.setString(1,"123");
psts.setString(2,"1234");
psts.addBatch();
}
psts.executeBatch();
psts.commit();
可以看到,JDBC
的addBatch
需要不断的向PreparedStatement
添加参数,然后调用addBatch()
方法。
上面的逻辑便是,首先判断添加的参数是不是需要循环添加的,如果是,则继续添加,如果SQL
不一样了,那么需要新建一个Statement
,然后再继续添加参数。
继续看我们可以看到,JDBC
在添加完参数以后,还需要执行依据executeBatch()
去真正的批量执行SQL
,那么MyBatis
将这一步放在哪里了呢?
BatchExecutor#doFlushStatements()
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
//判断是否已经回滚
if (isRollback) {
return Collections.emptyList();
}
//遍历处理所有的statement
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
//设置超时时间
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
//执行executeBatch
batchResult.setUpdateCounts(stmt.executeBatch());
//获取对应的statmenment
MappedStatement ms = batchResult.getMappedStatement();
//获取参数
List<Object> parameterObjects = batchResult.getParameterObjects();
//获取对应的主键处理器
KeyGenerator keyGenerator = ms.getKeyGenerator();
//处理主键
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
可以看到,这里代码虽然多,但是其实就是做了两个动作:
- 执行
executeBatch()
- 处理执行完后的主键
而这个flushStatement()
方法同时会在commit()
方法中被调用,因此一般也不需要我们手动调用。
ReuseExecutor
ReuseExecutor
是作为一个能将Statement
缓存起来进行复用的执行器。和其他执行主要不同的地方在于prepareStatement
:
ReuseExecutor#prepareStatement()
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
//通过sql 查询是否有缓存的`statement`
if (hasStatementFor(sql)) {
//如果有,直接进行复用
stmt = getStatement(sql);
//设置超时的值
applyTransactionTimeout(stmt);
} else {
//没有则新建,并进行缓存
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
可以看到,这样是节约了新建statement
的时间,对于PreperStatement
来说,同时也节约了预编译SQL
的时间。
但是我们可以发现一般默认的Executor
并不是REUSE
,而是SIMPLE
,为什么呢?因为从MyBatis
的执行流程来看,一个Executor
属于一个SqlSession
,而在MyBatis
中,并不推荐SqlSession
复用,一般一个方法对应于有一个SqlSession
,使用完以后需要关闭,因此很少存在能命中的情况。
当然如果是for
循环执行的话,那么应该建议改成ReuseExecutor
或者<for>
改写SQL
,因此一般默认SIMPLE
级别就足够使用。
以上便是MyBatis
中Executor
的秘密。这里我们再总结下已经查看的知识:
MyBatis
为了方便控制数据库的事务,通过在中间添加了一层Transaction
,使得MyBatis
可以接受其他的方式控制事务- 对于
MyBatis
的默认事务配置,直接使用JDBC
即可,也就是最简单的事务控制。 MyBatis
中设有3种不同的Executor
,分别是SIMPLE
,REUSE
,BATCH
,默认为SIMPLE
,REUSE
缓存了statment
,BATCH
底层使用了JDBC
的addBatch()
对应的方法- 如果要使得
BATCH
发挥效率,需要在MySql
的链接命令中添加useServerPrepStmts=false&rewriteBatchedStatements=true