mybatis
执行sql语句的操作是由执行器(Executor
)完成的,mybatis
中一共提供了3种Executor
:
类型 | 名称 | 功能 |
---|---|---|
REUSE |
重用执行器 | 缓存PreparedStatement ,下一次执行相同的sql 可重用 |
BATCH |
批量执行器 | 将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作 |
SIMPLE |
简单执行器 | 对每一次执行都生成PreparedStatement ,执行完就关闭,不缓存 |
另外,mybatis
还提供了一个缓存执行器CachingExecutor
,该执行器实际上是以上三种执行器的装饰类,用以处理缓存相关操作,实际干活的还是以上三种执行器之一。
Executor
的继续结构如下:
1. BaseExecutor
BaseExecutor
实现了Executor
的基本操作,如:
-
事务的处理:
commit(...)
:处理事务的提交rollback(...)
:处理事务的回滚
-
缓存的处理:
createCacheKey(...)
:创建缓存keyclearLocalCache(...)
:清除缓存
-
curd操作:
query(...)
:查询操作update(...)
:更新操作,插入与删除也是在这里处理
-
留待子类的实现
doUpdate(...)
:具体的更新操作,留待子类实现doQuery(...)
:具体的查询操作,留待子类实现
接下来我们关注Executor
的实现时,只关注留待子类实现的方法。
2. SimpleExecutor
SimpleExecutor
会对每一次执行都生成PreparedStatement
,执行完就关闭,不缓存,我们来看看它是怎么实现的,来看看它的doQuery(...)
方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {// 获取配置Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 得到 PrepareStatementstmt = prepareStatement(handler, ms.getStatementLog());// 执行查询return handler.query(stmt, resultHandler);} finally {// 关闭 StatementcloseStatement(stmt);}}
获取Statement
的方法为SimpleExecutor#prepareStatement
:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;// 获取数据库连接Connection connection = getConnection(statementLog);// 获取 Statementstmt = handler.prepare(connection, transaction.getTimeout());// 处理参数设置handler.parameterize(stmt);return stmt;}
这个方法先是获取了数据库连接,接着获取Statement
,然后处理了参数设置。
关于数据库连接的获取,我们在分析配置文件的解析时,数据源的配置最终会转化成PooledDataSource
或UnpooledDataSource
对象,数据库连接就是从数据源来的。
至于Statement
的生成,PreparedStatement
的实例化操作方法为PreparedStatementHandler#instantiateStatement
,这些都是常规的jdbc操作,就不细看了。
处理sql的执行方法为PreparedStatementHandler#query
:
@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;// 执行ps.execute();return resultSetHandler.handleResultSets(ps);}
SimpleExecutor#doQuery(...)
的执行流程如下:
- 获取数据库连接
- 获取
PrepareStatement
- 执行查询
- 关闭
PrepareStatement
SimpleExecutor
的操作就是常规的jdbc操作。
3. ReuseExecutor
ReuseExecutor
会缓存PreparedStatement
,下一次执行相同的sql
可重用。
我们依然分析doQuery(...)
方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 获取 StatementStatement stmt = prepareStatement(handler, ms.getStatementLog());// 处理查询操作return handler.query(stmt, resultHandler);}
与SimpleExecutor
相比,ReuseExecutor
的doQuery(...)
方法并没关闭Statement
.我们来看看Statement
的获取操作:
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 {// 没缓存,获取数据库连接,再获取 StatementConnection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());// 缓存 StatementputStatement(sql, stmt);}// 处理参数handler.parameterize(stmt);return stmt;
}
可以看到,ReuseExecutor
获取Statement
时,会先从缓存里获取,缓存里没有才会新建一个Statement
,然后将新建的Statement
添加到缓存中。从这里可以看出,ReuseExecutor
的Reuse,复用的是Statement
。
我们再来看看缓存Statement
的结构:
public class ReuseExecutor extends BaseExecutor {private final Map<String, Statement> statementMap = new HashMap<>();...private Statement getStatement(String s) {return statementMap.get(s);}private void putStatement(String sql, Statement stmt) {statementMap.put(sql, stmt);}
}
由些可见,缓存Statement
的是一个Map
,key
为sql
语句,value
为Statement
.
4. BatchExecutor
BatchExecutor
会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作,即:
- 进行修改操作(
insert
,update
,delete
)时,并不会立即执行,而是会缓存到本地 - 进行查询操作(
select
)时,会先处理缓存到本地的修改操作,再进行查询操作 - 也可行触发修改操作
从以上内容来看,这种方式似乎有大坑,列举几点如下:
- 修改操作缓存到本地后,如果执行前遇到意外重启,缓存的记录会不会丢失?
- 分布式环境下,多机共同协作,更新在A机上执行,查询在B机上执行,B机是不是不能查到B机的更新记录(B机的更新操作还在缓存中,并未执行)?
我们来看下BatchExecutor
的更新操作,进入doUpdate(...)
方法:
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,直接使用if (sql.equals(currentSql) && ms.equals(currentStatement)) {int last = statementList.size() - 1;stmt = statementList.get(last);applyTransactionTimeout(stmt);handler.parameterize(stmt);// fix Issues 322BatchResult batchResult = batchResultList.get(last);batchResult.addParameterObject(parameterObject);} else {// 创建连接,获取 StatementConnection connection = getConnection(ms.getStatementLog());stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt); // fix Issues 322currentSql = sql;currentStatement = ms;statementList.add(stmt);batchResultList.add(new BatchResult(ms, sql, parameterObject));}// 保存,等待之后批量执行handler.batch(stmt);return BATCH_UPDATE_RETURN_VALUE;}
BatchExecutor
有成员变量会记录上一次执行的sql
与MappedStatement
,如果本次执行的sql
与MappedStatement
与上一次执行的相同,则直接使用上一次的Statement
,否则就新建连接、获取Statement.
得到Statement
后,会调用PreparedStatementHandler#batch
方法:
public void batch(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.addBatch();}
这个方法并没有执行,只是调用PreparedStatement#addBatch
方法,将当前statement
保存了起来。
PreparedStatement#addBatch
方法如何使用呢?简单示意下:
// 获取连接
Connection connection = getConnection();
// 预编译sql
String sql = "xxx";
PreparedStatement statement = connection.prepareStatement(sql);
//记录1
statement.setInt(1, 1);
statement.setString(2, "one");
statement.addBatch();
//记录2
statement.setInt(1, 2);
statement.setString(2, "two");
statement.addBatch();
//记录3
statement.setInt(1, 3);
statement.setString(2, "three");
statement.addBatch();
//批量执行
int[] counts = statement.executeBatch();
// 关闭statment,关闭连接
...
BatchExecutor
的doUpdate(...)
方法并没有执行sql
语句,我们再来看看doQuery(...)
方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {// 处理缓存中的 statementsflushStatements();Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);// 获取连接,获取Statement,处理参数Connection connection = getConnection(ms.getStatementLog());stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);// 执行查询return handler.query(stmt, resultHandler);} finally {// 关闭 StatementcloseStatement(stmt);}}
doQuery(...)
方法会先调用flushStatements()
方法,然后再处理查询操作,整个过程基本同SimpleExecutor
一致,即”获取数据库连接-获取Statement
-处理查询-关闭Statement
“等几步。我们重点来看flushStatements()
方法的流程.
flushStatements()
方法最终调用的是BatchExecutor#doFlushStatements
方法,代码如下:
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {try {List<BatchResult> results = new ArrayList<>();if (isRollback) {return Collections.emptyList();}// 遍历的statementList,statementList就是缓存statement的结构for (int i = 0, n = statementList.size(); i < n; i++) {Statement stmt = statementList.get(i);applyTransactionTimeout(stmt);BatchResult batchResult = batchResultList.get(i);try {// 关键代码:stmt.executeBatch(),批量执行sqlbatchResult.setUpdateCounts(stmt.executeBatch());...} catch (BatchUpdateException e) {...}results.add(batchResult);}return results;} finally {...}}
BatchExecutor#doFlushStatements
方法的关键代码就是batchResult.setUpdateCounts(stmt.executeBatch())
;了 ,其中的stmt.executeBatch()
就是批量执行更新操作了。
从以上分析可知,BatchExecutor#doUpdate(...)
方法不会执行sql
语句,只是把sql
语句转换为Statement
然后缓存起来,在执行BatchExecutor#doQuery(...)
方法时,会先执行缓存起来的Statement
,然后再执行查询操作,当然也可以手动调用BatchExecutor#flushStatements
方法执行缓存的Statement
。
5. CachingExecutor
CachingExecutor
不同于以上3种执行器,它是一个装饰类,可以从缓存中获取数据,实际干活的还是以上三种执行器之一:
public class CachingExecutor implements Executor {// 具体的执行器private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}...
}
从代码来看,它是Executor
的子类,其中有一个成员变量delegate
,它的类型为Executor
,由构造方法传入。也就是说,在创建CachingExecutor
时,会传入以上3种执行器之一,CachingExecutor
会把它保存到成员变量delegate
中。
CachingExecutor的query(...)
方法如下:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 创建缓存keyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache = ms.getCache();// 操作缓存if (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler ** null) {ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")// 从缓存中获取 List<E> list = (List<E>) tcm.getObject(cache, key);if (list ** null) {// 实际处理查询的操作 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 添加到缓存中tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
从代码上来看,CachingExecutor
在处理查询时,会先从缓存中获取,当缓存中不存在时,就执行具体执行器的query(xxx)
方法。