精盡MyBatis原始碼分析 - SQL執行過程(一)之 Executor
該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址、Mybatis-Spring 原始碼分析 GitHub 地址、Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的SQL執行過程
在前面一系列的文件中,我已經分析了MyBatis的基礎支援層以及整個的初始化過程,此時MyBatis已經處於就緒狀態了,等待使用者發號施令了
那麼接下來我們來看看它執行SQL的過程,該過程比較複雜,因為涉及到二級快取,將返回結果轉換成Java物件和延遲載入等等處理過程,這裡將一步一步地進行分析
MyBatis中SQL執行的整體過程如下圖所示:
在SqlSession中,會將執行SQL的過程交由Executor
執行器去執行,過程大致如下:
- 通過
DefaultSqlSessionFactory
建立一個與資料的SqlSession
會話,其中會建立一個Executor
執行器物件 - 然後
Executor
執行器通過StatementHandler
建立對應的java.sql.Statement
物件,並通過ParameterHandler
設定引數,然後操作資料庫 - 如果是更新資料的操作,則可能需要通過
KeyGenerator
設定自增鍵,返回受影響的行數 - 如果是查詢操作,則將操作資料庫返回的
ResultSet
結果集物件包裝成ResultSetWrapper
,然後通過DefaultResultSetHandler
對結果集進行對映,返回Java物件
上面還涉及到一級快取、二級快取和延遲載入等其他處理過程
SQL執行過程(一)之Executor
在MyBatis的SQL執行過程中,Executor執行器擔當著一個重要的角色,相關操作都需要通過它來執行,相當於一個排程器,把SQL語句交給它,它來呼叫各個元件執行操作
其中一級快取和二級快取都是在Executor執行器中完成的
Executor
執行器介面的實現類如下圖所示:
-
org.apache.ibatis.executor.BaseExecutor
:實現Executor介面,提供骨架方法,支援一級快取,指定幾個抽象的方法交由不同的子類去實現 -
org.apache.ibatis.executor.SimpleExecutor
:繼承 BaseExecutor 抽象類,簡單的 Executor 實現類(預設) -
org.apache.ibatis.executor.ReuseExecutor
:繼承 BaseExecutor 抽象類,可重用的 Executor 實現類,相比SimpleExecutor,在Statement執行完操作後不會立即關閉,而是快取起來,執行的SQL作為key,下次執行相同的SQL時優先從快取中獲取Statement物件 -
org.apache.ibatis.executor.BatchExecutor
:繼承 BaseExecutor 抽象類,支援批量執行的 Executor 實現類 -
org.apache.ibatis.executor.CachingExecutor
:實現 Executor 介面,支援二級快取的 Executor 的實現類,實際採用了裝飾器模式,裝飾物件為左邊三個Executor類
Executor
org.apache.ibatis.executor.Executor
:執行器介面,程式碼如下:
public interface Executor {
/**
* ResultHandler 空物件
*/
ResultHandler NO_RESULT_HANDLER = null;
/**
* 更新或者插入或者刪除
* 由傳入的 MappedStatement 的 SQL 所決定
*/
int update(MappedStatement ms, Object parameter) throws SQLException;
/**
* 查詢,帶 ResultHandler + CacheKey + BoundSql
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey cacheKey, BoundSql boundSql) throws SQLException;
/**
* 查詢,帶 ResultHandler
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException;
/**
* 查詢,返回 Cursor 遊標
*/
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
/**
* 刷入批處理語句
*/
List<BatchResult> flushStatements() throws SQLException;
/**
* 提交事務
*/
void commit(boolean required) throws SQLException;
/**
* 回滾事務
*/
void rollback(boolean required) throws SQLException;
/**
* 建立 CacheKey 物件
*/
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
/**
* 判斷是否快取
*/
boolean isCached(MappedStatement ms, CacheKey key);
/**
* 清除本地快取
*/
void clearLocalCache();
/**
* 延遲載入
*/
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
/**
* 獲得事務
*/
Transaction getTransaction();
/**
* 關閉事務
*/
void close(boolean forceRollback);
/**
* 判斷事務是否關閉
*/
boolean isClosed();
/**
* 設定包裝的 Executor 物件
*/
void setExecutorWrapper(Executor executor);
}
執行器介面定義了操作資料庫的相關方法:
- 資料庫的讀和寫操作
- 事務相關
- 快取相關
- 設定延遲載入
- 設定包裝的 Executor 物件
BaseExecutor
org.apache.ibatis.executor.BaseExecutor
:實現Executor介面,提供骨架方法,指定幾個抽象的方法交由不同的子類去實現,例如:
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds,
BoundSql boundSql) throws SQLException;
上面這四個方法交由不同的子類去實現,分別是:更新資料庫、刷入批處理語句、查詢資料庫和查詢資料返回遊標
構造方法
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
/**
* 事務物件
*/
protected Transaction transaction;
/**
* 包裝的 Executor 物件
*/
protected Executor wrapper;
/**
* DeferredLoad(延遲載入)佇列
*/
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
/**
* 本地快取,即一級快取,內部就是一個 HashMap 物件
*/
protected PerpetualCache localCache;
/**
* 本地輸出型別引數的快取,和儲存過程有關
*/
protected PerpetualCache localOutputParameterCache;
/**
* 全域性配置
*/
protected Configuration configuration;
/**
* 記錄當前會話正在查詢的數量
*/
protected int queryStack;
/**
* 是否關閉
*/
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
其中上面的屬性可根據註釋進行檢視
這裡提一下localCache
屬性,本地快取,用於一級快取,MyBatis的一級快取是什麼呢?
每當我們使用 MyBatis 開啟一次和資料庫的會話,MyBatis 都會創建出一個 SqlSession 物件,表示與資料庫的一次會話,而每個 SqlSession 都會建立一個 Executor 物件
在對資料庫的一次會話中,我們有可能會反覆地執行完全相同的查詢語句,每一次查詢都會訪問一次資料庫,如果在極短的時間內做了完全相同的查詢,那麼它們的結果極有可能完全相同,由於查詢一次資料庫的代價很大,如果不採取一些措施的話,可能造成很大的資源浪費
為了解決這一問題,減少資源的浪費,MyBatis 會在每一次 SqlSession 會話物件中建立一個簡單的快取,將每次查詢到的結果快取起來,當下次查詢的時候,如果之前已有完全一樣的查詢,則會先嚐試從這個簡單的快取中獲取結果返回給使用者,不需要再進行一次資料庫查詢了