Mybatis執行器
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);
// 得到 PrepareStatement
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行查詢
return handler.query(stmt, resultHandler);
} finally {
// 關閉 Statement
closeStatement(stmt);
}
}
獲取Statement的方法為SimpleExecutor#prepareStatement:
private Statement prepareStatement(StatementHandler handler, Log statementLog)
throws SQLException {
Statement stmt;
// 獲取數據庫連接
Connection connection = getConnection(statementLog);
// 獲取 Statement
stmt = handler.prepare(connection, transaction.getTimeout());
// 處理參數設置
handler.parameterize(stmt);
return stmt;
}
這個方法先是獲取了數據庫連接,接著獲取Statement,然后處理了參數設置。
關于數據庫連接的獲取,我們在分析配置文件的解析時,數據源的配置最終會轉化成PooledDataSource或UnpooledDataSource對象,數據庫連接就是從數據源來的。
至于Statement的生成,PreparedStatement的實例化操作方法為PreparedStatementHandler#instantiateStatement,這些都是常規的jdbc操作,就不細看了。
處理sql的執行方法為PreparedStatementHandler#query:
@Override
public <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);
// 獲取 Statement
Statement 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 {
// 沒緩存,獲取數據庫連接,再獲取 Statement
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
// 緩存 Statement
putStatement(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 322
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;
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 {
// 處理緩存中的 statements
flushStatements();
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 {
// 關閉 Statement
closeStatement(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(),批量執行sql
batchResult.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);
// 創建緩存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <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)方法。

浙公網安備 33010602011771號