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

月圓吖發表於2020-11-24

該系列文件是本人在學習 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執行的整體過程如下圖所示:

SQLExecuteProcess

在 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執行器介面的實現類如下圖所示:

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

如下圖所示,MyBatis 的一次會話:在一個 SqlSession 會話物件中建立一個localCache本地快取,對於每一次查詢,都會根據查詢條件嘗試去localCache本地快取中獲取快取資料,如果存在,就直接從快取中取出資料然後返回給使用者,否則訪問資料庫進行查詢,將查詢結果存入快取並返回給使用者(如果設定的快取區域為STATEMENT,預設為SESSION,在一次會話中所有查詢執行後會清空當前 SqlSession 會話中的localCache本地快取,相當於“關閉”了一級快取

所有的資料庫更新操作都會清空當前 SqlSession 會話中的本地快取

LevelOneCache

如上描述,MyBatis的一級快取在多個 SqlSession 會話時,可能導致資料的不一致性,某一個 SqlSession 更新了資料而其他 SqlSession 無法獲取到更新後的資料,出現資料不一致性,這種情況是不允許出現了,所以我們通常選擇“關閉”一級快取

clearLocalCache方法

clearLocalCache()方法,清空一級(本地)快取,如果全域性配置中設定的localCacheScope快取區域為STATEMENT(預設為SESSION),則在每一次查詢後會呼叫該方法,相當於關閉了一級快取,程式碼如下:

@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
        localOutputParameterCache.clear();
    }
}

createCacheKey方法

createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)方法,根據本地查詢的相關資訊建立一個CacheKey快取key物件,程式碼如下:

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // <1> 建立 CacheKey 物件
    CacheKey cacheKey = new CacheKey();
    // <2> 設定 id、offset、limit、sql 到 CacheKey 物件中
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    // <3> 設定 ParameterMapping 陣列的元素對應的每個 value 到 CacheKey 物件中
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) { // 該引數需要作為入參
            Object value;
            String propertyName = parameterMapping.getProperty();
            /*
             * 獲取該屬性值
             */
            if (boundSql.hasAdditionalParameter(propertyName)) {
              // 從附加引數中獲取
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
              // 入參物件為空則直接返回 null
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
              // 入參有對應的型別處理器則直接返回該引數
                value = parameterObject;
            } else {
              // 從入參物件中獲取該屬性的值
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);
        }
    }
    // <4> 設定 Environment.id 到 CacheKey 物件中
    if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}
  1. 建立一個CacheKey例項物件

  2. 將入參中的idoffsetlimitsql,通過CacheKeyupdate方法新增到其中,它的方法如下:

    public void update(Object object) {
        // 方法引數 object 的 hashcode
        int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
        this.count++;
        // checksum 為 baseHashCode 的求和
        this.checksum += baseHashCode;
         // 計算新的 hashcode 值
        baseHashCode *= this.count;
        this.hashcode = this.multiplier * this.hashcode + baseHashCode;
        // 新增 object 到 updateList 中
        this.updateList.add(object);
    }
    
  3. 獲取本次查詢的入參值,通過CacheKeyupdate方法新增到其中

  4. 獲取本次環境的Environment.id,通過CacheKeyupdate方法新增到其中

  5. 返回CacheKey例項物件,這樣就可以為本次查詢生成一個唯一的快取key物件,可以看看CacheKey重寫的equal方法:

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!(object instanceof CacheKey)) {
            return false;
        }
        final CacheKey cacheKey = (CacheKey) object;
    
        if (hashcode != cacheKey.hashcode) {
            return false;
        }
        if (checksum != cacheKey.checksum) {
            return false;
        }
        if (count != cacheKey.count) {
            return false;
        }
        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cacheKey.updateList.get(i);
            if (!ArrayUtil.equals(thisObject, thatObject)) {
                return false;
            }
        }
        return true;
    }
    

query相關方法

查詢資料庫因為涉及到一級快取,所以這裡有多層方法,最終訪問資料庫的doQuery方法是交由子類去實現的,總共分為三層:

  1. 根據入參獲取BoundSql和CacheKey物件,然後再去呼叫查詢方法

  2. 涉及到一級快取和延遲載入的處理,快取未命中則再去呼叫查詢資料庫的方法

  3. 儲存一些資訊供一級快取使用,內部呼叫doQuery方法執行資料庫的讀操作

接下來我們分別來看看這三個方法

query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)方法,資料庫查詢操作的入口,程式碼如下

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
        throws SQLException {
    // <1> 獲得 BoundSql 物件
    BoundSql boundSql = ms.getBoundSql(parameter);
    // <2> 建立 CacheKey 物件
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // <3> 查詢
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
  1. 通過MappedStatement物件根據入參獲取BoundSql物件,在《MyBatis初始化(四)之SQL初始化(下)》中的SqlSource小節中有講到這個方法,如果是動態SQL則需要進行解析,獲取到最終的SQL,替換成?佔位符
  2. 呼叫createCacheKey方法為本次查詢建立一個CacheKey物件
  3. 繼續呼叫query(...)方法執行查詢

query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法,處理資料庫查詢操作,涉及到一級快取,程式碼如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
        CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    // <1> 已經關閉,則丟擲 ExecutorException 異常
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // <2> 清空本地快取,如果 queryStack 為零,並且要求清空本地快取(配置了 flushCache = true)
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        // <3> queryStack + 1
        queryStack++;
        // <4> 從一級快取中,獲取查詢結果
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) { // <4.1> 獲取到,則進行處理
            // 處理快取儲存過程的結果
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else { // <4.2> 獲得不到,則從資料庫中查詢
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        // <5> queryStack - 1
        queryStack--;
    }
    if (queryStack == 0) { // <6> 如果當前會話的所有查詢執行完了
        // <6.1> 執行延遲載入
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        // <6.2> 清空 deferredLoads
        deferredLoads.clear();
        // <6.3> 如果快取級別是 LocalCacheScope.STATEMENT ,則進行清理
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    // <7> 返回查詢結果
    return list;
}
  1. 當前會話已經被關閉則丟擲異常

  2. 如果queryStack0(表示是當前會話只有本次查詢而沒有其他的查詢了),並且要求清空本地快取(配置了flushCache=true),那麼直接清空一級(本地)快取

  3. 當前會話正在查詢的數量加一,queryStack++

  4. localCache一級快取獲取快取的查詢結果

    1. 如果有快取資料,則需要處理儲存過程的情況,將需要作為出參(OUT)的引數設定到本次查詢的入參的屬性中
    2. 如果沒有快取資料,則呼叫queryFromDatabase方法,執行資料庫查詢操作
  5. 當前會話正在查詢的數量減一,queryStack--

  6. 如果當前會話所有查詢都執行完

    1. 執行當前會話中的所有的延遲載入deferredLoads,這種延遲載入屬於查詢後的延遲,和後續講到的獲取屬性時再載入不同,這裡的延遲載入是在哪裡生成的呢?

      DefaultResultSetHandler中進行結果對映時,如果某個屬性配置的是子查詢,並且本次的子查詢在一級快取中有快取資料,那麼將會建立一個DeferredLoad物件儲存在deferredLoads中,該屬性值先設定為DEFERRED延遲載入物件(final修飾的Object物件),待當前會話所有的查詢結束後,也就是當前執行步驟,則會從一級快取獲取到資料設定到返回結果中

    2. 清空所有的延遲載入deferredLoads物件

    3. 如果全域性配置的快取級別為STATEMENT(預設為SESSION),則清空當前會話中一級快取的所有資料

  7. 返回查詢結果

queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法,執行資料庫查詢操作,程式碼如下:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
        ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // <1> 在快取中,新增正在執行的佔位符物件,因為正在執行的查詢不允許提前載入需要延遲載入的屬性,可見 DeferredLoad#canLoad() 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // <2> 執行讀操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // <3> 從快取中,移除佔位物件
        localCache.removeObject(key);
    }
    // <4> 新增到快取中
    localCache.putObject(key, list);
    // <5> 如果是儲存過程,則將入參資訊儲存儲存,跟一級快取處理儲存過程相關
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    // <6> 返回查詢結果
    return list;
}
  1. 在快取中,新增正在執行的EXECUTION_PLACEHOLDER佔位符物件,因為正在執行的查詢不允許提前載入需要延遲載入的屬性,可見 DeferredLoad#canLoad() 方法
  2. 呼叫查詢資料庫doQuery方法,該方法交由子類實現
  3. 刪除第1步新增的佔位符
  4. 將查詢結果新增到localCache一級快取
  5. 如果是儲存過程,則將入參資訊儲存儲存,跟一級快取處理儲存過程相關,可見上面的第個方法的第4.1
  6. 返回查詢結果

update方法

update(MappedStatement ms, Object parameter)方法,執行更新資料庫的操作,程式碼如下:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    // <1> 已經關閉,則丟擲 ExecutorException 異常
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // <2> 清空本地快取
    clearLocalCache();
    // <3> 執行寫操作
    return doUpdate(ms, parameter);
}
  1. 當前會話已經被關閉則丟擲異常
  2. 清空當前會話中一級快取的所有資料
  3. 呼叫更新資料庫doUpdate方法,該方法交由子類實現

其他方法

除了上面介紹的幾個重要的方法以外,還有其他很多方法,例如獲取當前事務,提交事務,回滾事務,關閉會話等等,這裡我就不一一列出來了,請自行閱讀該類

SimpleExecutor

org.apache.ibatis.executor.SimpleExecutor:繼承 BaseExecutor 抽象類,簡單的 Executor 實現類(預設使用

  • 每次對資料庫的操作,都會建立對應的Statement物件

  • 執行完成後,關閉該Statement物件

程式碼如下:

public class SimpleExecutor extends BaseExecutor {

	public SimpleExecutor(Configuration configuration, Transaction transaction) {
		super(configuration, transaction);
	}

	@Override
	public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
		Statement stmt = null;
		try {
			Configuration configuration = ms.getConfiguration();
			// 建立 StatementHandler 物件
			StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
			// 初始化 Statement 物件
			stmt = prepareStatement(handler, ms.getStatementLog());
			// 通過 StatementHandler 執行寫操作
			return handler.update(stmt);
		} finally {
            // 關閉 Statement 物件
			closeStatement(stmt);
		}
	}

	@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);
			// 初始化 Statement 物件
			stmt = prepareStatement(handler, ms.getStatementLog());
			// 通過 StatementHandler 執行讀操作
			return handler.query(stmt, resultHandler);
		} finally {
			// 關閉 Statement 物件
			closeStatement(stmt);
		}
	}

	@Override
	protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
			throws SQLException {
		Configuration configuration = ms.getConfiguration();
		StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
		Statement stmt = prepareStatement(handler, ms.getStatementLog());
		Cursor<E> cursor = handler.queryCursor(stmt);
		stmt.closeOnCompletion();
		return cursor;
	}

	@Override
	public List<BatchResult> doFlushStatements(boolean isRollback) {
		return Collections.emptyList();
	}

	private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
		Statement stmt;
		// 獲得 Connection 物件,如果開啟了 Debug 模式,則返回的是一個代理物件
		Connection connection = getConnection(statementLog);
		// 建立 Statement 或 PrepareStatement 物件
		stmt = handler.prepare(connection, transaction.getTimeout());
		// 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
		handler.parameterize(stmt);
		return stmt;
	}
}

我們看到這些方法的實現,其中的步驟差不多都是一樣的

  1. 獲取Configuration全域性配置物件

  2. 通過上面全域性配置物件的newStatementHandler方法,建立RoutingStatementHandler物件,採用了裝飾器模式,根據配置的StatementType建立對應的物件,預設為PreparedStatementHandler物件,進入BaseStatementHandler的構造方法你會發現有幾個重要的步驟,在後續會講到?

    然後使用外掛鏈對該物件進行應用,方法如下所示:

    // Configuration.java
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
            Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        /*
         * 建立 RoutingStatementHandler 路由物件
         * 其中根據 StatementType 建立對應型別的 Statement 物件,預設為 PREPARED
         * 執行的方法都會路由到該物件
         */
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, 
                                                                        rowBounds, resultHandler, boundSql);
        // 將 Configuration 全域性配置中的所有外掛應用在 StatementHandler 上面
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }
    
  3. 呼叫prepareStatement方法初始化Statement物件

    1. 從事務中獲取一個Connection資料庫連線,如果開啟了Debug模式,則會為該Connection建立一個動態代理物件的例項,用於列印Debug日誌
    2. 通過上面第2步建立的StatementHandler物件建立一個Statement物件(預設為PrepareStatement),還會進行一些準備工作,例如:如果配置了KeyGenerator(設定主鍵),則會設定需要返回相應自增鍵,在後續會講到?
    3. Statement物件中設定SQL的引數,例如PrepareStatement?佔位符,實際上是通過DefaultParameterHandler設定佔位符引數,在前面的《MyBatis初始化(四)之SQL初始化(下)》中有講到
    4. 返回已經建立好的Statement物件,就等待著執行資料庫操作了
  4. 通過StatementHandlerStatement進行資料庫的操作,如果是查詢操作則會通過DefaultResultSetHandler進行引數對映(非常複雜,後續逐步分析?)

ReuseExecutor

org.apache.ibatis.executor.ReuseExecutor:繼承 BaseExecutor 抽象類,可重用的 Executor 實現類

  • 每次對資料庫的操作,優先從當前會話的快取中獲取對應的Statement物件,如果不存在,才進行建立,建立好了會放入快取中
  • 資料庫操作執行完成後,不關閉該Statement物件
  • 其它的和SimpleExecutor是一致的

我們來看看他的prepareStatement方法就好了:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    /*
     * 根據需要執行的 SQL 語句判斷 是否已有對應的 Statement 並且連線未關閉
     */
    if (hasStatementFor(sql)) {
        // 從快取中獲得 Statement 物件
        stmt = getStatement(sql);
        // 重新設定事務超時時間
        applyTransactionTimeout(stmt);
    } else {
        // 獲得 Connection 物件
        Connection connection = getConnection(statementLog);
        // 初始化 Statement 物件
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 將 Statement 新增到快取中,key 值為 當前執行的 SQL 語句
        putStatement(sql, stmt);
    }
    // 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
    handler.parameterize(stmt);
    return stmt;
}

在建立Statement物件前,會根據本次查詢的SQL從本地的Map<String, Statement> statementMap獲取到對應的Statement物件

  1. 如果快取命中,並且該物件的連線未關閉,那麼重新設定當前事務的超時時間

  2. 如果快取未命中,則執行和SimpleExecutor中的prepareStatement方法相同邏輯建立一個Statement物件並放入statementMap快取中

BatchExecutor

org.apache.ibatis.executor.BatchExecutor:繼承 BaseExecutor 抽象類,支援批量執行的 Executor 實現類

  • 我們在執行資料庫的更新操作時,可以通過StatementaddBatch()方法將資料庫操作新增到批處理中,等待呼叫StatementexecuteBatch()方法進行批處理

  • BatchExecutor維護了多個Statement物件,一個物件對應一個SQL(sqlMappedStatement物件都相等),每個Statement物件對應多個資料庫操作(同一個sql多種入參),就像蘋果藍裡裝了很多蘋果,番茄藍裡裝了很多番茄,最後,再統一倒進倉庫

由於JDBC不支援資料庫查詢的批處理,所以這裡就不展示它資料庫查詢的實現方法,和SimpleExecutor一致,我們來看看其他的方法

構造方法

public class BatchExecutor extends BaseExecutor {

	public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;

	/**
	 * Statement 陣列
	 */
	private final List<Statement> statementList = new ArrayList<>();
	/**
	 * BatchResult 陣列
	 *
	 * 每一個 BatchResult 元素,對應 {@link #statementList} 集合中的一個 Statement 元素
	 */
	private final List<BatchResult> batchResultList = new ArrayList<>();
	/**
	 * 上一次新增至批處理的 Statement 物件對應的SQL
	 */
	private String currentSql;
	/**
	 * 上一次新增至批處理的 Statement 物件對應的 MappedStatement 物件
	 */
	private MappedStatement currentStatement;

	public BatchExecutor(Configuration configuration, Transaction transaction) {
		super(configuration, transaction);
	}
}
  • statementList屬性:維護多個Statement物件

  • batchResultList屬性:維護多個BatchResult物件,每個物件對應上面的一個Statement物件,每個BatchResult物件包含同一個SQL和其每一次操作的入參

  • currentSql屬性:上一次新增至批處理的Statement物件對應的SQL

  • currentStatement屬性:上一次新增至批處理的Statement物件對應的MappedStatement物件

BatchResult

org.apache.ibatis.executor.BatchResult:相同SQL(sqlMappedStatement物件都相等)聚合的結果,包含了同一個SQL每一次操作的入參,程式碼如下:

public class BatchResult {

	/**
	 * MappedStatement 物件
	 */
	private final MappedStatement mappedStatement;
	/**
	 * SQL
	 */
	private final String sql;
	/**
	 * 引數物件集合
	 *
	 * 每一個元素,對應一次操作的引數
	 */
	private final List<Object> parameterObjects;

	/**
	 * 更新數量集合
	 *
	 * 每一個元素,對應一次操作的更新數量
	 */
	private int[] updateCounts;

	public BatchResult(MappedStatement mappedStatement, String sql) {
		super();
		this.mappedStatement = mappedStatement;
		this.sql = sql;
		this.parameterObjects = new ArrayList<>();
	}

	public BatchResult(MappedStatement mappedStatement, String sql, Object parameterObject) {
		this(mappedStatement, sql);
		addParameterObject(parameterObject);
	}

	public void addParameterObject(Object parameterObject) {
		this.parameterObjects.add(parameterObject);
	}
}

doUpdate方法

更新資料庫的操作,新增至批處理,需要呼叫doFlushStatements執行批處理,程式碼如下:

@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    // <1> 建立 StatementHandler 物件
    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;
    // <2> 如果和上一次新增至批處理 Statement 物件對應的 currentSql 和 currentStatement 都一致,則聚合到 BatchResult 中
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
        // <2.1> 獲取上一次新增至批處理 Statement 物件
        int last = statementList.size() - 1;
        stmt = statementList.get(last);
        // <2.2> 重新設定事務超時時間
        applyTransactionTimeout(stmt);
        // <2.3> 往 Statement 中設定 SQL 語句上的引數,例如 PrepareStatement 的 ? 佔位符
        handler.parameterize(stmt);// fix Issues 322
        // <2.4> 獲取上一次新增至批處理 Statement 對應的 BatchResult 物件,將本次的入參新增到其中
        BatchResult batchResult = batchResultList.get(last);
        batchResult.addParameterObject(parameterObject);
    } else { // <3> 否則,建立 Statement 和 BatchResult 物件
        // <3.1> 初始化 Statement 物件
        Connection connection = getConnection(ms.getStatementLog());
        stmt = handler.prepare(connection, transaction.getTimeout());
        handler.parameterize(stmt); // fix Issues 322
        // <3.2> 設定 currentSql 和 currentStatemen
        currentSql = sql;
        currentStatement = ms;
        // <3.3> 新增 Statement 到 statementList 中
        statementList.add(stmt);
        // <3.4> 建立 BatchResult 物件,並新增到 batchResultList 中
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // <4> 新增至批處理
    handler.batch(stmt);
    // <5> 返回 Integer.MIN_VALUE + 1002
    return BATCH_UPDATE_RETURN_VALUE;
}
  1. 建立StatementHandler物件,和SimpleExecutor中一致,在後續會講到?

  2. 如果和上一次新增至批處理Statement物件對應的currentSqlcurrentStatement都一致,則聚合到BatchResult

    1. 獲取上一次新增至批處理Statement物件
    2. 重新設定事務超時時間
    3. Statement中設定 SQL 語句上的引數,例如PrepareStatement?佔位符,在SimpleExecutor中已經講到
    4. 獲取上一次新增至批處理Statement對應的BatchResult物件,將本次的入參新增到其中
  3. 否則,建立StatementBatchResult物件

    1. 初始化Statement物件,在SimpleExecutor中已經講到,這裡就不再重複了
    2. 設定currentSqlcurrentStatemen屬性
    3. 新增StatementstatementList集合中
    4. 建立BatchResult物件,並新增到batchResultList集合中
  4. 新增至批處理

  5. 返回Integer.MIN_VALUE + 1002,為什麼返回這個值?不清楚

doFlushStatements方法

執行批處理,也就是將之前新增至批處理的資料庫更新操作進行批處理,程式碼如下:

@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
        List<BatchResult> results = new ArrayList<>();
        if (isRollback) { // <1> 如果 isRollback 為 true ,返回空陣列
            return Collections.emptyList();
        }
        // <2> 遍歷 statementList 和 batchResultList 陣列,逐個提交批處理
        for (int i = 0, n = statementList.size(); i < n; i++) {
            // <2.1> 獲得 Statement 和 BatchResult 物件
            Statement stmt = statementList.get(i);
            applyTransactionTimeout(stmt);
            BatchResult batchResult = batchResultList.get(i);
            try {
                // <2.2> 提交該 Statement 的批處理
                batchResult.setUpdateCounts(stmt.executeBatch());
                MappedStatement ms = batchResult.getMappedStatement();
                List<Object> parameterObjects = batchResult.getParameterObjects();
              /*
               * <2.3> 獲得 KeyGenerator 物件
               * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
               * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
               * 否則為 NoKeyGenerator 物件
               */
                KeyGenerator keyGenerator = ms.getKeyGenerator();
                if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
                    Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
                    // <2.3.1> 批處理入參物件集合,設定自增鍵
                    jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
                } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { // issue #141
                    for (Object parameter : parameterObjects) {
                      // <2.3.1> 一次處理每個入參物件,設定自增鍵
                        keyGenerator.processAfter(this, ms, stmt, parameter);
                    }
                }
                // Close statement to close cursor #1109
                // <2.4> 關閉 Statement 物件
                closeStatement(stmt);
            } catch (BatchUpdateException e) {
                // 如果發生異常,則丟擲 BatchExecutorException 異常
                StringBuilder message = new StringBuilder();
                message.append(batchResult.getMappedStatement().getId())
                        .append(" (batch index #")
                        .append(i + 1)
                        .append(")")
                        .append(" failed.");
                if (i > 0) {
                    message.append(" ")
                            .append(i)
                            .append(" prior sub executor(s) completed successfully, but will be rolled back.");
                }
                throw new BatchExecutorException(message.toString(), e, results, batchResult);
            }
            // <2.5> 新增到結果集
            results.add(batchResult);
        }
        return results;
    } finally {
        // <3.1> 關閉 Statement 們
        for (Statement stmt : statementList) {
            closeStatement(stmt);
        }
        // <3.2> 置空 currentSql、statementList、batchResultList 屬性
        currentSql = null;
        statementList.clear();
        batchResultList.clear();
    }
}

在呼叫doUpdate方法將資料庫更新操作新增至批處理後,我們需要呼叫doFlushStatements方法執行批處理,邏輯如下:

  1. 如果isRollbacktrue,表示需要回退,返回空陣列

  2. 遍歷statementListbatchResultList陣列,逐個提交批處理

    1. 獲得StatementBatchResult物件
    2. 提交該Statement的批處理
    3. 獲得KeyGenerator物件,用於設定自增鍵,在後續會講到?
    4. 關閉Statement物件
    5. BatchResult物件新增到結果集
  3. 最後會關閉所有的Statement和清空當前會話中儲存的資料

二級快取

BaseExecutor中講到的一級快取中,快取資料僅在當前的 SqlSession 會話中進行共享,可能會導致多個 SqlSession 出現資料不一致性的問題

如果需要在多個 SqlSession 之間需要共享快取資料,則需要使用到二級快取

開啟二級快取後,會使用CachingExecutor物件裝飾其他的Executor類,這樣會先在CachingExecutor進行二級快取的查詢,快取未命中則進入裝飾的物件中,進行一級快取的查詢

流程如下圖所示:

LevelTwoCache

《MyBatis初始化》的一系列文件中講過MappedStatement會有一個Cache物件,是根據@CacheNamespace註解或<cache />標籤建立的物件,該物件也會儲存在Configuration全域性配置物件的Map<String, Cache> caches = new StrictMap<>("Caches collection")中,key為所在的namespace,也可以通過@CacheNamespaceRef註解或<cache-ref />標籤來指定其他namespace的Cache物件

在全域性配置物件中cacheEnabled是否開啟快取屬性預設為true,可以在mybatis-config.xml配置檔案中新增以下配置關閉:

<configuration>
    <settings>
        <setting name="cacheEnabled" value="false" />
    </settings>
</configuration>

我們來看看MyBatis是如何實現二級快取

CachingExecutor

org.apache.ibatis.executor.CachingExecutor:實現 Executor 介面,支援二級快取的 Executor 的實現類

構造方法
public class CachingExecutor implements Executor {
	/**
	 * 被委託的 Executor 物件
	 */
	private final Executor delegate;
	/**
	 * TransactionalCacheManager 物件
	 */
	private final TransactionalCacheManager tcm = new TransactionalCacheManager();

	public CachingExecutor(Executor delegate) {
		this.delegate = delegate;
		// 設定 delegate 被當前執行器所包裝
		delegate.setExecutorWrapper(this);
	}
}
  • delegate 屬性,為被委託的Executor物件,具體的資料庫操作都是交由它去執行
  • tcm 屬性,TransactionalCacheManager物件,支援事務的快取管理器,因為二級快取是支援跨 SqlSession 共享的,此處需要考慮事務,那麼,必然需要做到事務提交時,才將當前事務中查詢時產生的快取,同步到二級快取中,所以需要通過TransactionalCacheManager來實現
query方法

處理資料庫查詢操作的方法,涉及到二級快取,會將Cache二級快取物件裝飾成TransactionalCache物件並存放在TransactionalCacheManager管理器中,程式碼如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
        ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // <1> 獲取 Cache 二級快取物件
    Cache cache = ms.getCache();
    // <2> 如果配置了二級快取
    if (cache != null) {
        // <2.1> 如果需要清空快取,則進行清空
        flushCacheIfRequired(ms);
        // <2.2> 如果當前操作需要使用快取(預設開啟)
        if (ms.isUseCache() && resultHandler == null) {
            // <2.2.1> 如果是儲存過程相關操作,保證所有的引數模式為 ParameterMode.IN
            ensureNoOutParams(ms, boundSql);
    	    // <2.2.2> 從二級快取中獲取結果,會裝飾成 TransactionalCache
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // <2.2.3> 如果不存在,則從資料庫中查詢
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // <2.2.4> 將快取結果儲存至 TransactionalCache
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
    		// <2.2.5> 直接返回結果
            return list;
        }
    }
    // <3> 沒有使用二級快取,則呼叫委託物件的方法
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
  1. 獲取Cache二級快取物件

  2. 如果該物件不為空,表示配置了二級快取

    1. 如果需要清空快取,則進行清空
    2. 如果當前操作需要使用快取(預設開啟)
      1. 如果是儲存過程相關操作,保證所有的引數模式為ParameterMode.IN
      2. 通過TransactionalCacheManager從二級快取中獲取結果,會裝飾成TransactionalCach物件
      3. 如果快取未命中,則呼叫委託物件的query方法
      4. 將快取結果儲存至TransactionalCache物件中,並未真正的儲存至Cache二級快取中,需要待事務提交才會儲存過去,其中快取未命中的也會設定快取結果為null
      5. 直接返回結果
  3. 沒有使用二級快取,則呼叫委託物件的方法

update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
	// 如果需要清空快取,則進行清空
    flushCacheIfRequired(ms);
	// 執行 delegate 對應的方法
    return delegate.update(ms, parameterObject);
}

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        tcm.clear(cache);
    }
}

資料庫的更新操作,如果配置了需要清空快取,則清空二級快取

這裡就和一級快取不同,一級快取是所有的更新操作都會清空一級快取

commit方法
@Override
public void commit(boolean required) throws SQLException {
	// 執行 delegate 對應的方法
    delegate.commit(required);
	// 提交 TransactionalCacheManager
    tcm.commit();
}

在事務提交後,通過TransactionalCacheManager二級快取管理器,將本次事務生成的快取資料從TransactionalCach中設定到正真的Cache二級快取中

rollback方法
@Override
public void rollback(boolean required) throws SQLException {
    try {
        // 執行 delegate 對應的方法
        delegate.rollback(required);
    } finally {
        if (required) {
            // 回滾 TransactionalCacheManager
            tcm.rollback();
        }
    }
}

在事務回滾後,如果需要的話,通過TransactionalCacheManager二級快取管理器,將本次事務生成的快取資料從TransactionalCach中移除

close方法
@Override
public void close(boolean forceRollback) {
    try {
        // issues #499, #524 and #573
        if (forceRollback) {
            tcm.rollback();
        } else {
            tcm.commit();
        }
    } finally {
        delegate.close(forceRollback);
    }
}

在事務關閉前,如果是強制回滾操作,則TransactionalCacheManager二級快取管理器,將本次事務生成的快取資料從TransactionalCach中移除,否則還是將快取資料設定到正真的Cache二級快取中

TransactionalCacheManager

org.apache.ibatis.cache.TransactionalCacheManager:二級快取管理器,因為二級快取是支援跨 SqlSession 共享的,所以需要通過它來實現,當事務提交時,才將當前事務中查詢時產生的快取,同步到二級快取中,程式碼如下:

public class TransactionalCacheManager {
	/**
	 * Cache 和 TransactionalCache 的對映
	 */
	private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

	public void clear(Cache cache) {
		getTransactionalCache(cache).clear();
	}

	public Object getObject(Cache cache, CacheKey key) {
		return getTransactionalCache(cache).getObject(key);
	}

	public void putObject(Cache cache, CacheKey key, Object value) {
		// 首先,獲得 Cache 對應的 TransactionalCache 物件
    	// 然後,新增 KV 到 TransactionalCache 物件中
		getTransactionalCache(cache).putObject(key, value);
	}

	public void commit() {
		for (TransactionalCache txCache : transactionalCaches.values()) {
			txCache.commit();
		}
	}

	public void rollback() {
		for (TransactionalCache txCache : transactionalCaches.values()) {
			txCache.rollback();
		}
	}

	private TransactionalCache getTransactionalCache(Cache cache) {
		return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
	}

}
  • getTransactionalCache(Cache cache)方法,根據Cache二級快取物件獲取對應的TransactionalCache物件,如果沒有則建立一個儲存起來

  • getObject(Cache cache, CacheKey key)方法,會先呼叫getTransactionalCache(Cache cache)方法獲取對應的TransactionalCache物件,然後根據CacheKey從該物件中獲取快取結果

  • putObject(Cache cache, CacheKey key, Object value)方法,同樣也先呼叫getTransactionalCache(Cache cache)方法獲取對應的TransactionalCache物件,根據該物件將結果進行快取

  • commit()方法,遍歷transactionalCaches,依次呼叫TransactionalCache的提交方法

  • rollback()方法,遍歷transactionalCaches,依次呼叫TransactionalCache的回滾方法

TransactionalCache

org.apache.ibatis.cache.decorators.TransactionalCache:用來裝飾二級快取的物件,作為二級快取一個事務的緩衝區

在一個SqlSession會話中,該類包含所有需要新增至二級快取的的快取資料,當提交事務後會全部刷出到二級快取中,或者事務回滾後移除這些快取資料,程式碼如下:

public class TransactionalCache implements Cache {

	private static final Log log = LogFactory.getLog(TransactionalCache.class);

	/**
	 * 委託的 Cache 物件。
	 *
	 * 實際上,就是二級快取 Cache 物件。
	 */
	private final Cache delegate;
	/**
	 * 提交時,清空 {@link #delegate}
	 *
	 * 初始時,該值為 false
	 * 清理後{@link #clear()} 時,該值為 true ,表示持續處於清空狀態
     *
     * 因為可能事務還未提交,所以不能直接清空所有的快取,而是設定一個標記,獲取快取的時候返回 null 即可
     * 先清空下面這個待提交變數,待事務提交的時候才真正的清空快取
     *
	 */
	private boolean clearOnCommit;
	/**
	 * 待提交的 Key-Value 對映
	 */
	private final Map<Object, Object> entriesToAddOnCommit;
	/**
	 * 查詢不到的 KEY 集合
	 */
	private final Set<Object> entriesMissedInCache;

	public TransactionalCache(Cache delegate) {
		this.delegate = delegate;
		this.clearOnCommit = false;
		this.entriesToAddOnCommit = new HashMap<>();
		this.entriesMissedInCache = new HashSet<>();
	}

	@Override
	public Object getObject(Object key) {
		// issue #116
		// <1> 從 delegate 中獲取 key 對應的 value
		Object object = delegate.getObject(key);
		if (object == null) {// <2> 如果不存在,則新增到 entriesMissedInCache 中
			entriesMissedInCache.add(key);
		}
		// issue #146
		if (clearOnCommit) {// <3> 如果 clearOnCommit 為 true ,表示處於持續清空狀態,則返回 null
			return null;
		} else {
			return object;
		}
	}

	@Override
	public void putObject(Object key, Object object) {
		// 暫存 KV 到 entriesToAddOnCommit 中
		entriesToAddOnCommit.put(key, object);
	}

	@Override
	public void clear() {
    	// <1> 標記 clearOnCommit 為 true
		clearOnCommit = true;
   	 	// <2> 清空 entriesToAddOnCommit
		entriesToAddOnCommit.clear();
	}

	public void commit() {
		// <1> 如果 clearOnCommit 為 true ,則清空 delegate 快取
		if (clearOnCommit) {
			delegate.clear();
		}
		// 將 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
		flushPendingEntries();
		// 重置
		reset();
	}

	public void rollback() {
		// <1> 從 delegate 移除出 entriesMissedInCache
		unlockMissedEntries();
		// <2> 重置
		reset();
	}

	private void reset() {
		clearOnCommit = false;
		entriesToAddOnCommit.clear();
		entriesMissedInCache.clear();
	}

	private void flushPendingEntries() {
		for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
			delegate.putObject(entry.getKey(), entry.getValue());
		}
		for (Object entry : entriesMissedInCache) {
			if (!entriesToAddOnCommit.containsKey(entry)) {
				delegate.putObject(entry, null);
			}
		}
	}

	private void unlockMissedEntries() {
		for (Object entry : entriesMissedInCache) {
			try {
				delegate.removeObject(entry);
			} catch (Exception e) {
				log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
						+ "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
			}
		}
	}
}

根據上面的註釋檢視每個屬性的作用,我們依次來看下面的方法,看看在不同事務之前是如何處理二級快取的

  • putObject(Object key, Object object)方法,新增快取資料時,先把快取資料儲存在entriesToAddOnCommit中,這個物件屬於當前事務,事務還未提交,其他事務是不能訪問到的

  • clear()方法,設定clearOnCommit標記為true,告訴當前事務正處於持續清空狀態,先把entriesToAddOnCommit清空,也就是當前事務中還未提交至二級快取的快取資料,事務還未提交,不能直接清空二級快取中的資料,否則影響到其他事務了

  • commit()方法,事務提交後,如果clearOnCommit為true,表示正處於持續清空狀態,需要先把二級快取中的資料全部清空,然後再把當前事務生成的快取設定到二級快取中,然後重置當前物件

    這裡為什麼處於清空狀態把二級快取的資料清空後,還要將當前事務生成的快取資料再設定到二級快取中呢?因為當前事務呼叫clear()方法後可能有新生成了新的快取資料,而不能把這些忽略掉

  • getObject(Object key)方法

    1. 先從delegate二級快取物件中獲取結果
    2. 如果快取未命中則將該key新增到entriesMissedInCache屬性中,因為二級快取也會將快取未命中的key起來,資料為null
    3. 如果clearOnCommit為true,即使你快取命中了也返回null,因為觸發clear()方法的話,本來需要清空二級快取的,但是事務還未提交,所以先標記一個快取持續清理的這麼一個狀態,這樣相當於在當前事務中既清空了二級快取資料,也不影響其他事務的二級快取資料
    4. 返回獲取到的結果,可能為null

Executor在哪被建立

前面對Executor執行器介面以及實現類都有分析過,那麼它是在哪建立的呢?

《MyBatis初始化(一)之載入mybatis-config.xml》這一篇文件中講到,整個的初始化入口在SqlSessionFactoryBuilderbuild方法中,建立的是一個DefaultSqlSessionFactory物件,該物件用來建立SqlSession會話的,我們來瞧一瞧:

public class DefaultSqlSessionFactory implements SqlSessionFactory {

	private final Configuration configuration;

	@Override
	public SqlSession openSession() {
		return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
	}

	private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
			boolean autoCommit) {
		Transaction tx = null;
		try {
			// 獲得 Environment 物件
			final Environment environment = configuration.getEnvironment();
			// 建立 Transaction 物件
			final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
			tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
			// 建立 Executor 物件
			final Executor executor = configuration.newExecutor(tx, execType);
			// 建立 DefaultSqlSession 物件
			return new DefaultSqlSession(configuration, executor, autoCommit);
		} catch (Exception e) {
			// 如果發生異常,則關閉 Transaction 物件
			closeTransaction(tx); // may have fetched a connection so lets call close()
			throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
		} finally {
			ErrorContext.instance().reset();
		}
	}
}

我們所有的資料庫操作都是在MyBatis的一個SqlSession會話中執行的,在它被建立的時候,會先通過Configuration全域性配置物件的newExecutor方法建立一個Executor執行器

newExecutor(Transaction transaction, ExecutorType executorType)方法,根據執行器型別建立執行Executor執行器,程式碼如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // <1> 獲得執行器型別
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    // <2> 建立對應實現的 Executor 物件
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    // <3> 如果開啟快取,建立 CachingExecutor 物件,進行包裝
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // <4> 應用外掛
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
  1. 獲得執行器型別,預設為SIMPLE
  2. 建立對應的Executor物件,預設就是SimpleExecutor執行器了
  3. 如果全域性配置了開啟二級快取,則將Executor物件,封裝成CachingExecutor物件
  4. 外掛鏈應用該物件,在後續會講到?

總結

本文分析了MyBatis在執行SQL的過程中,都是在SimpleExecutor(預設型別)執行器中進行的,由它呼叫其他“元件”來完成資料庫操作

其中需要通過PrepareStatementHandler(預設)來建立對應的PrepareStatemen,進行引數的設定等相關處理,執行資料庫操作

獲取到結果後還需要通過DefaultResultSetHandler進行引數對映,轉換成對應的Java物件,這兩者在後續會進行分析

關於MyBatis的快取,存在侷限性,我們通常不會使用,如有需要使用快取,檢視我的另一篇原始碼解析文件《JetCache原始碼分析》

一級快取

僅限於單個 SqlSession 會話,多個 SqlSession 可能導致資料的不一致性,例如某一個 SqlSession 更新了資料而其他 SqlSession 無法獲取到更新後的資料,出現資料不一致性,這種情況是不允許出現了

二級快取

MyBatis配置二級快取是通過在XML對映檔案新增<cache / >標籤建立的(註解也可以),所以不同的XML對映檔案所對應的二級快取物件可能不是同一個

二級快取雖然解決的一級快取中存在的多個 SqlSession 會話可能出現髒讀的問題,但還是針對同一個二級快取物件不會出現這種情況,如果其他的XML對映檔案修改了相應的資料,當前二級快取獲取到的快取資料就不是最新的資料,也出現了髒讀的問題

例如,在一個XML對映檔案中配置了二級快取,獲取到某個使用者的資訊並存放在對應的二級快取物件中,其他的XML對映檔案修改了這個使用者的資訊,那麼之前那個快取資料就不是最新的

當然你可以XML對映檔案對指向同一個Cache物件(通過<cache-ref / >標籤),這樣就太侷限了,所以MyBatis的快取存在一定的缺陷,且快取的資料僅僅是儲存在了本地記憶體中,對於當前高併發的環境下是無法滿足要求的,所以我們通常不使用MyBatis的快取

參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》

相關文章