Mybatis 查詢 流程分析

前面知道了SqlSession 初始化過程,那麼下一步就看看Mybatis具體的增刪改查邏輯。

本文以以下幾個問題開展:

  1. Mybatis Mapper 代理對象獲取流程
  2. 動態sql查詢時,if,foreach等節點是怎麼處理的?
  3. Mybatis動態Sql對如何讀取參數的?
  4. @Param註解什麼作用,什麼情況沒有@Param註解會讀取不到參數?
  5. 對於結果集,Mybatis是如何處理的?
  6. Mybatis 一級二級緩存如何使用?

Mybatis代理對象

當使用 Mybatis是,我們只需要定義接口,而後定義對應 xml文件,就可以完成增刪改查,接口對象怎麼創建呢?

  1. 通過SqlSession 的 getMapper
  public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
  }
  1. Configuration中 的getMapper
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry 中保存了來自 xml配置下所有Mappers,並放到一個 Map<Class<?>, MapperProxyFactory<?>> 保存所有接口文件對應的 Mapper 構造器。

  1. 最後在 MapperRegistry 的 通過 在初始化中構造好的 MapperProxyFactorynewInstance,生成一個 由MapperProxy 包裝的動態對象。
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

而後通過JDK 反射獲取對應接口:

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

以上返回了一個動態代理類,該類實現了 mapperInterface 接口,並且以 MapperProxy 作爲默認包裝代理。
所以當使用 執行代理類方法,首先會進入 MapperProxyinvoke方法。

Select 查詢分析

Mybatis 整體流程是比較好理解的,好理解的前提起始是我們都忽略了 其強大的XML解析配置,以及對動態SQL的強大支持。下面看看查詢:

  1. 執行對應查詢方法,例如 mapper.listAllActivedUsers(actived, list);
  2. 進入MapperProxyinvoke方法:期間會創建一個 MapperMethod 類,用於構建 代表增刪改查的 SqlComand;以及方法簽名包裝類 MethodSignature,裏面放着其返回值,是否返回list,是否返回的Cursor 等。
  3. MapperMethod 中,主要是判斷 SqlComand 類型,增刪改還是查,最後返回查詢初的結果。
  4. 當Mybatis 判斷爲查詢多個時候,則會進入 MapperMethodexcuteForMany 方法,這裏面重要的代碼是 Object param = method.convertArgsToSqlCommandParam(args);,param是解析 參數後的 Map。
    它會將傳入參數以<key,value> 存儲,以 @Param 的值作爲key,並且還會固定以參數順序存儲一份 param1, param2, param3等固定名字參數,所以param中容量是實際傳入的一倍
  5. 判斷是否有傳入 RowBounds分頁參數,有則加載進入。
  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    ... 
  1. DefaultSqlSession 對其進行 查詢,其中會如果參數是collection或者數組,則會進行封裝一層。
  2. 動態sql 拼裝邏輯看下一節
  3. 查詢時,首先會執行 CachingExecutorquery,這一層主要對 一級緩存進行相應邏輯判斷,一級緩存默認開啓:
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      // 判斷 MappedStatement 是否有緩存
    Cache cache = ms.getCache();
    if (cache != null) {
     // 有cache 判斷是否要刪除緩存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 從 TransactionalCacheManager 加載緩存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        // 查詢後加入緩存
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  1. 而後拿着句柄就往下執行 BaseExecutorquery 方法, 在 裏面有個 對 ErrorContext 進行的設定,關於 ErrorContext 可以看下下節。

  2. 如果第一次 查詢,則會清除 localCachelocalOutputParameterCache 緩存,而下面對該次查詢進行了重入性判斷,當再次查詢時則會直接從 localCache 獲取。
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

  3. 下一步即從數據庫中查詢,默認使用 SimpleExecutor進行 操作,應此Statement 也是一次性使用

  @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);
      // 執行 prepare,即預檢查
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

prepareStatement:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
  1. 最後就是在 PreparedStatementHandler執行 sql,執行完後 使用 DefaultResultSetHandler.handleResultSets 處理結果。
  2. 處理查詢結果主要將 查出結果 ResultSet 封裝成結果返回,中途還會 處理 自定義 ResultSet 以及Lazy Loading 相關。下面基於 DefaultResultSetHandlerhandleResultSets
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
  // 設置當前動作
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    final List<Object> multipleResults = new ArrayList<>();
	// 設置檢索
    int resultSetCount = 0;
    // 從 Statment 中獲取 ResultSet 並封裝
    ResultSetWrapper rsw = getFirstResultSet(stmt);
  // 獲取ResultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    // 計算resultMaps 數量
    int resultMapCount = resultMaps.size();
    // 簡單校驗result map 數量
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
    // 獲取resultMap
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 處理結果成 resultMap形式,並放入multipleResult
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 下一個resultSetWrapper
      rsw = getNextResultSet(stmt);
      // 將當前nestedResultObjects 內容
      cleanUpAfterHandlingResultSet();
      // 如果有則下一個
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
    // 嵌套查詢
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
	// 封裝成list返回
    return collapseSingleResultList(multipleResults);
  }

動態SQL解析

下一個關鍵點就在 Mybatis的ongl表達式解析了,或許會有這樣一個問題,爲什麼傳入的參數mybatis可以理解,並且可以以動態sql形式解析,支持 if 和 foreach 等邏輯?
MappedStatementgetBoundSql 可以獲取一個BoundSqlBoundSql 對象是一個從 SqlSource 中分析之後獲取的 動態對象。最終 能從 xml 對象中將 動態sql解析出含有 和 參數的字段。

  1. 從 sqlSource中獲取對象:
  public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  1. 而後,在DynamicSqlSource中的 getBoundSql 進行進一步解析,在 DynamicSqlSource 中有變量 SqlNode, 動態sql主要組裝邏輯就在 SqlNode 子類中。
    在這裏插入圖片描述
    上面 SqlNode大致作用如下:
  • StaticTextSqlNode: 裏面有個String 類型 text,主要是匹配xml中靜態sql
  • MixedSqlNode : 裏面有 List<SqlNode> ,主要是裝載多種混合類型SqlNode,即可以爲多種 SqlNode 的組合
  • TextSqlNode:用於存儲解析 包含 ${} 佔位符的動態SQL節點。將動態SQL,帶 ${} 解析完成SQL語句的解析其,即將${}佔位符替換成實際的變量值。
  • ForEachSqlNode:用於存儲解析 <foreach> 節點值。
  • IfSqlNode : 用於解析 <if> 節點值,由於<if> 下面可能又是一個節點,所以裏面還有一個 SqlNode contents
  • VarDeclSqlNode :處理動態xml標籤<bind>的SqlNode類。
  • TrimSqlNode: 用於處理 <trim> 節點 的 SqlNode。
  • ChooseSqlNode: 用於處理 <choose> 標籤的工具類。
    DynamicSqlSource 的 getBoundSql:
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 遞歸的處理動態sql節點
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

上面代碼中,使用 rootSqlNode.apply(context); 解析動態sql節點,而後使用 SqlSourceBuilder,將 # 開頭的佔位符,全部使用 ? 替代,而後將解析出來參數設置到上下文 DynamicContext 中。
另外,對於處理 <foreach> 的邏輯,是將所有參數都以 ? 填充,而增加相應個數參數從而可以適配循環參數。

ErrorContext 作用

當使用Mybatis時候,是否會發現這樣的感覺,如果哪個環節出錯,能夠迅速的找出問題,或者sql少寫了一個點,或者 哪個 <if> 標籤判斷出錯導致參數錯誤,或者是參數或者結果解析出問題,都能馬上定位到問題。
這不是你厲害,而是Mybatis告訴你哪裏出錯了>-<

Mybatis就是使用ErrorContext 來幫你記錄你sql運行的過程。
例如 : ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

看到 staticThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); 是一個很典型的 ThreadLocal 用法,每一次與數據庫交互的查詢,都能夠被當前上下文保存下來。
ErrorContext 的構造方法是私有的,使用 instance 創建而成的 單例模式。

  1. resource:說明是加載哪一個資源,主要爲 xml的Mapper文件
  2. activity:說明什麼動作,例如查詢就是 executing a query
  3. object:說明是執行mapper文件中,哪個語句,例如 anla.learn.mybatis.interceptor.dao.UserMapper.listAllActivedUsers
  4. message: 用於保存出錯信息,調用點爲 ExceptionFactory,而傳入的message主要爲 特定錯誤類型:
    在這裏插入圖片描述
  5. sql:存入 boundSql中的sql,也就是通過動態sql解析過後的sql。在 BaseStatementHandler 中的 prepare 調用,所以所有與數據庫交互語句都會在這裏初始化 sql。
  6. cause : 主要存儲錯誤棧,配合 上面 message 使用。

在每次執行完並且沒有錯,都會將當前 ErrorContext 清除。

一級緩存和二級緩存

其實一級緩存二級緩存命名比較彆扭,因爲事實上,如果開啓了二級緩存,則最終緩存調用順序爲:
二級緩存->一級緩存->數據庫。因爲二級默認不開啓,所以一般是一級緩存,如果一級緩存中沒有,則到數據庫。
一二級緩存這樣區分,主要是Mybatis上有相關注釋說明了 TransactionalCacheManager 爲二級緩存。

  • 一級緩存:MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
  • 二級緩存
    二級緩存存儲位置是 CachingExecutorTransactionalCacheManager 中,默認不開啓。

下面簡單分析下一二級緩存及其原理:

一級緩存

一級緩存在BaseExecutorPerpetualCache 中,默認開啓。作用域默認是 SESSION,可以選擇爲STATEMENT。如果爲SESSION,則在整個SqlSession中都會共享該緩存,而如果爲Statement,則本次查詢結束,則會清除該緩存。

  @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());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 看是否需要清空緩存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 嘗試從緩存中讀取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      // 直接查詢數據庫
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 如果是STATEMENT 域緩存,則用完直接清空
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

一級緩存實現原理較爲簡單, PerpetualCache裏面主要 定義一個 Map<Object, Object> cache 類型全局變量,實現了緩存操作。
總結:

  1. 一級緩存默認開啓,默認爲 SESSION級別
  2. Mybatis 一級緩存聲明週期和 SqlSession一致,數據只在 SqlSession內部(配置爲SESSION時)
  3. Mybatis一級緩存設計爲一個沒有容量的HashMap。
  4. 以及緩存範圍爲SqlSession,在多個SqlSession或者分佈式下,會引起髒數據,建議設定爲 STATEMENT。

二級緩存

二級緩存主要使用實際上在一級緩存之前就會調用,在 CachingExecutorquery 中,但是需要設置Setting:

<setting name="cacheEnabled" value="true"/>

實際在 3.5.2 中, 這個默認是開啓。
第二個是需要在xml中增加 <cache/> 標籤。並配置好namespace

從而在 讀取配置時,會初始化 MappedStatement 中Cache,在 CachingExecutor 中會進行引用:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      // 如果配置了 二級緩存以及相應namespace,則會有Cache
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 沒有則需要進行查詢
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

二級緩存開啓後,同一個namespace下的所有操作語句,都影響着同一個Cache,即二級緩存被多個SqlSession共享,是一個全局的變量。

覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,一起研究Mybatis:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章