主頁 > 後端開發 > 精盡MyBatis原始碼分析 - SQL執行程序(一)之 Executor

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

2020-11-24 22:33:09 後端開發

該系列檔案是本人在學習 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 物件以及延遲加載等等處理程序,這里將一步一步地進行分析:

  • 《SQL執行程序(一)之Executor》
  • 《SQL執行程序(二)之StatementHandler》
  • 《SQL執行程序(三)之ResultSetHandler》
  • 《SQL執行程序(四)之延遲加載》

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 = https://www.cnblogs.com/lifullmoon/archive/2020/11/24/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 = https://www.cnblogs.com/lifullmoon/archive/2020/11/24/Integer.MIN_VALUE + 1002;

	/**
	 * Statement 陣列
	 */
	private final List statementList = new ArrayList<>();
	/**
	 * BatchResult 陣列
	 *
	 * 每一個 BatchResult 元素,對應 {@link #statementList} 集合中的一個 Statement 元素
	 */
	private final List 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="https://www.cnblogs.com/lifullmoon/archive/2020/11/24/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 原始碼分析》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/227061.html

標籤:其他

上一篇:THINKPHP5截取標題長度并加省略號

下一篇:THINKPHP5截取標題長度并加省略號

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more