精盡MyBatis原始碼分析 - SQL執行過程(一)之 Executor
阿新 • • 發佈:2020-11-24
> 該系列文件是本人在學習 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 會話物件中建立一個簡單的快取,將每次查詢到的結果快取起來,當下次查詢的時候,如果之前已有完全一樣的查詢,則會先嚐試從這個簡單的快取中獲取結果返回給使用者,不需要再進行一次資料庫查詢了