SqlSession 的四大對象是指:Executor, StatementHandler,ParameterHandler,ResultHandler對象。Mybatis通過四大對象的相互協作,完成對數據庫的操作。
這篇我們主要講解 Eexcutor對象。
我們觀察先一下SqlSession的實現類DefaultSqlSession中的一些方法。
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
//拿到被解析過的sql聲明對象
MappedStatement ms = configuration.getMappedStatement(statement);
//調用執行器進行數據庫操作
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
//調用執行器進行數據庫操作
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
可以發現,SqlSession拿到被解析過的sql聲明對象後,將其交給 Executor對象進行處理。
1. 繼承關係
executor見名之意就是執行sql操作的執行器。Executor的實現類有很多,具體的繼承關係如下圖
- CachingExecutor:這是一個緩存器,在cacheEnabled配置開啓時,他會作爲Executor的一個裝飾器存在,攔截Executor的執行。mybatis的二級緩存就由它實現。
- SimpleExecutor :普通的執行器。
- ReuseExecutor:可重用執行器,其維護了一個Map<String, Statement>,將執行的sql作爲key,將執行的Statement作爲value保存,這樣執行相同的sql時就可以使用已經存在的Statement。
- BatchExecutor:批處理執行器,他會將相同的操作暫存,然後進行批處理。
在默認情況下,mybatis爲我們實例化的是SimpleExecutor,也可以通過配置修改實例化的執行器類型。
2. SimpleExecutor 簡單執行器
這裏選取 SimpleExecutor的 doQuery方法來看看
@Override
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對象,他也是四大對象之一
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//獲取到statment對象
stmt = prepareStatement(handler, ms.getStatementLog());
//調用 StatementHandler對象進行查詢操作,並得到返回結果
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//通過Connection獲取到statment對象,並設置了一些配置參數
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
可以看到,Executor首先獲取到了StatementHandler 對象,並調用其方法獲取到Statment對象,然後設置了一些配置參數。最後就直接調用了StatementHandler對象的query方法進行查詢操作,且直接獲取到查詢結果了。所以能發現SimpleExecutor並沒有做任何特殊的處理。
3. ReuseExecutor 重用執行器
這裏依然選取doQuery方法來看看
public class ReuseExecutor extends BaseExecutor {
//通過維護這個對象來實現重用
private final Map<String, Statement> statementMap = new HashMap<>();
//這個方法與SimpleExecutor的沒什麼差別,差別在 prepareStatement 方法中
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 stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
//獲取到執行的sql語句
String sql = boundSql.getSql();
//在statementMap 中尋找sql對應的statment
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
//與SimpleExecutor相同,獲取到statment對象
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
//將statment對象緩存到statementMap 中
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
}
可以看到,ReuseExecutor將需要執行的sql語句以及其對應的statement緩存到 statementMap中,下次執行相同的sql時,就可以從緩存中直接拿到與其對應的statement對象。
4. BatchExecutor 批處理執行器
批處理執行器這用到了 jdbc中批處理功能。我們先來看看 doUpdate方法
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
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和statment和上一次的相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
//獲取到上一次的statment對象
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
//對上次的statment對象設置本次的參數
handler.parameterize(stmt);//fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
//獲取到 Connection 和 statment對象
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
//設置參數
handler.parameterize(stmt); //fix Issues 322
//這裏將 本次的sql和statment保存到全局變量中
currentSql = sql;
currentStatement = ms;
//將statment保存進list
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
//添加批處理 關鍵就是看這裏
handler.batch(stmt);
//返回一個固定的值
return BATCH_UPDATE_RETURN_VALUE;
}
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//實際上就是調用了statment的addBatch方法,並沒有對sql進行執行
ps.addBatch();
}
這裏有三個特殊的地方需要我們關注:
- 方法進來時,首先會判斷當前的sql和statement對象是否和上一次執行的相同。如果相同就直接取上一次的statement對象,然後設置本次操作的參數即可。 這樣設計就是因爲在批處理的場景中,會連續出現很多相同的sql語句。對相同操作複用同一個statement對象,節省了創建statement對象的開銷。
- doUpdate方法封裝好statement對象後,調用了Statementhandler的batch方法,然而這個方法只是調用了 statement的addBatch方法,並沒有對sql進行執行。所以能發現,在BatchExecutor中,增刪改操作並不會立即執行,而是會將其存儲起來,等待另一個事件觸發執行。
- 此方法由於並沒有真實的去執行sql,所以默認返回了一個常量,這個常量並不能作爲sql語句是否執行成功的依據。
然後我們來看看 doQuery方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
//關鍵看這裏
flushStatements();
//下面就是標準的執行流程
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
能發現,doQuery方法在執行前,做了一次刷新操作,這個操作最終由 doFlushStatements方法完成。
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
if (isRollback) {
return Collections.emptyList();
}
//遍歷整個 statementList
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
//這裏纔是真正調用了 statment的executorBatch方法完成了批處理操作
batchResult.setUpdateCounts(stmt.executeBatch());
......
// Close statement to close cursor #1109
closeStatement(stmt);
} catch (BatchUpdateException e) {......}
results.add(batchResult);
}
return results;
} finally {......}
}
很明顯,上面說到的觸發批處理執行的事件就是這裏是執行查詢語句。當執行查詢語句時,BatchStatment會將之前保存的增刪改操作一併提交執行。
5. CachingExecutor 緩存器
之所以最後說這個執行器,是因爲他與上面的三個執行器並不屬於同一個類型。他永遠是作爲上面三個執行器的裝飾器的存在,並不參與sql執行的過程。
//這個是CacheExeCutor中維護的 被裝飾對象
private final Executor delegate;
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);
}
很明顯,在執行查詢操作時,CacheExecutor會先嚐試從緩存中獲取結果,獲取不到時,再調用被裝飾類進行查詢。