注:本系列源碼分析基於mybatis 3.5.6,源碼的gitee倉庫倉庫地址:funcy/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)
方法。
本文原文鏈接:https://my.oschina.net/funcy/blog/4952948 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。