在 MyBatis
中有关 Executor
的配置如下:
设置名 | 描述 | 有效值 | 默认值 |
---|---|---|---|
defaultExecutorType
|
配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
也就是说,在 MyBatis
中有三种 Executor
:
SimpleExecutor
: 就是普通的执行器。
ReuseExecutor
: 执行器会重用预处理语句( PreparedStatement
)
BatchExecutor
: 批量执行器,底层调用 JDBC
中 Statement#batch()
Executor
的作用?
首先看 SqlSession
的一个查询方法的源代码:
@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
很像是设计模式中的模板方法模式,它实现了 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
是一个最简单的执行器,没有做额外的操作。
首先看 Executor
的 update()
方法
@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
会执行批处理。
下面看看体现这些功能的代码:
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
是在我们需要循环/批量执行某些操作的时候,可以进行试用,其底层使用的 statment#addBatch()
方法,一般对于 mysql
,要想此方法发挥其性能的优点,需要在连接的时候添加以下两个命令:
useServerPrepStmts=false rewriteBatchedStatements=true
@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
将这一步放在哪里了呢?
@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
是作为一个能将 Statement
缓存起来进行复用的执行器。和其他执行主要不同的地方在于 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