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

精盡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執行器去執行,過程大致如下:

  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:執行器介面,程式碼如下:

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 會話物件中建立一個簡單的快取,將每次查詢到的結果快取起來,當下次查詢的時候,如果之前已有完全一樣的查詢,則會先嚐試從這個簡單的快取中獲取結果返回給使用者,不需要再進行一次資料庫查詢了