1. 程式人生 > >精盡MyBatis原始碼分析 - SQL執行過程(一)之 Executor

精盡MyBatis原始碼分析 - SQL執行過程(一)之 Executor

> 該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋([Mybatis原始碼分析 GitHub 地址](https://github.com/liu844869663/mybatis-3)、[Mybatis-Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring)、[Spring-Boot-Starter 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-boot-starter))進行閱讀 > > MyBatis 版本:3.5.2 > > MyBatis-Spring 版本:2.0.3 > > MyBatis-Spring-Boot-Starter 版本:2.1.4 ## MyBatis的SQL執行過程 在前面一系列的文件中,我已經分析了 MyBatis 的基礎支援層以及整個的初始化過程,此時 MyBatis 已經處於就緒狀態了,等待使用者發號施令了 那麼接下來我們來看看它執行SQL的整個過程,該過程比較複雜,涉及到二級快取,將返回結果轉換成 Java 物件以及延遲載入等等處理過程,這裡將一步一步地進行分析: - [**《SQL執行過程(一)之Executor》**](https://www.cnblogs.com/lifullmoon/p/14015111.html) - **《SQL執行過程(二)之StatementHandler》** - **《SQL執行過程(三)之ResultSetHandler》** - **《SQL執行過程(四)之延遲載入》** MyBatis中SQL執行的整體過程如下圖所示:
在 SqlSession 中,會將執行 SQL 的過程交由`Executor`執行器去執行,過程大致如下: 1. 通過`DefaultSqlSessionFactory`建立與資料庫互動的 `SqlSession` “會話”,其內部會建立一個`Executor`執行器物件 2. 然後`Executor`執行器通過`StatementHandler`建立對應的`java.sql.Statement`物件,並通過`ParameterHandler`設定引數,然後執行資料庫相關操作 3. 如果是資料庫**更新**操作,則可能需要通過`KeyGenerator`先設定自增鍵,然後返回受影響的行數 4. 如果是資料庫**查詢**操作,則需要將資料庫返回的`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`:執行器介面,程式碼如下: ```java public interface Executor { /** * ResultHandler 空物件 */ ResultHandler NO_RESULT_HANDLER = null; /** * 更新或者插入或者刪除 * 由傳入的 MappedStatement 的 SQL 所決定 */ int update(MappedStatement ms, Object parameter) throws SQLException; /** * 查詢,帶 ResultHandler + CacheKey + BoundSql */ List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; /** * 查詢,帶 ResultHandler */ List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; /** * 查詢,返回 Cursor 遊標 */ Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; /** * 刷入批處理語句 */ List 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介面,提供骨架方法,指定幾個抽象的方法交由不同的子類去實現,例如: ```java protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; protected abstract List doFlushStatements(boolean isRollback) throws SQLException; protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; ``` 上面這四個方法交由不同的子類去實現,分別是:更新資料庫、刷入批處理語句、查詢資料庫和查詢資料返回遊標 #### 構造方法 ```java 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 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 會話物件中建立一個簡單的快取,將每次查詢到的結果快取起來,當下次查詢的時候,如果之前已有完全一樣的查詢,則會先嚐試從這個簡單的快取中獲取結果返回給使用者,不需要再進行一次資料庫查詢了