1. 程式人生 > >sqlSession讀寫資料庫完全解析

sqlSession讀寫資料庫完全解析

1 引言和主要類

建立完sqlSession例項後,我們就可以進行資料庫操作了。比如通過selectOne()方法查詢資料庫,如程式碼

// 讀取XML配置檔案
String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 建立sqlSessionFactory單例,初始化mybatis容器
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 建立sqlSession例項,用它來進行資料庫操作,mybatis執行時的門面
SqlSession session = sessionFactory.openSession(); // 進行資料庫查詢操作 User user = session.selectOne("test.findUserById", 1); // 訪問資料庫,statement為mapper.xml中的id

建立sqlSessionFactory單例和sqlSession例項在前兩節中分析過了,下面我們著重來分析sqlSession操作資料庫的過程。

sqlSession操作資料庫有兩種方法,可以直接使用select update insert delete等方法;也可以通過getMapper()先獲取mapper動態代理例項,然後再進行資料庫操作。相比而言mapper方式更靈活且不易出錯,是mybatis推薦的方式。本節我們分析直接使用select等方法的流程,下一節再分析mapper方式。

本節以selectOne()方法的實現過程為例來分析sqlSession操作資料庫的流程,涉及的主要類如下。

  1. DefaultSqlSession:SqlSession的預設實現,其方法基本都是利用Executor代理實現。
  2. Executor:mybatis執行的核心,排程器。排程mybatis其他三大元件的執行。
  3. StatementHandler:SQL語句執行器,cache的管理等
  4. ParameterHandler:入參處理器,statementType為PREPARE是需要使用到它,來解析入參到preparedStatement中
  5. ResultSetHandler:結果集對映處理器,將資料庫操作原始結果(主要是查詢操作),對映為Java POJO。這正是ORM要解決的關鍵問題。

2 流程

2.1 DefaultSqlSession的selectOne()

先從DefaultSqlSession的selectOne()方法看起。

public <T> T selectOne(String statement, Object parameter) {
  // selectOne本質上是呼叫selectList實現,如果結果集大於一個,則報TooManyResultsException。
  List<T> list = this.<T>selectList(statement, parameter);
  if (list.size() == 1) {
    return list.get(0);
  } else if (list.size() > 1) {
    throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
  } else {
    return null;
  }
}

public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

  // 由sql語句的標示statement和入參parameter,查詢滿足條件的資料列表
  // @Param statement: mapper.xml中mapper節點下的select delete update insert等子節點的id屬性
  // @Param parameter: 傳入sql語句的入參
  // @Param rowBounds: 邏輯分頁,包含offset和limit兩個主要成員變數。mybatis分頁邏輯為捨棄offset之前條目,取剩下的limit條。預設DEFAULT不分頁
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 從mappers節點初始化階段建立好的mappedStatements這個Map中,找到key為當前要找到的sql的id的那條
      MappedStatement ms = configuration.getMappedStatement(statement);

      // 通過執行器Executor作為總排程來執行查詢語句,後面以BaseExecutor來分析。
      // BatchExecutor ReuseExecutor SimpleExecutor均繼承了BaseExecutor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

selectOne()方法其實是通過呼叫selectList()實現的,因為二者本質是完全相同的,只是前者返回一個物件,而後者為列表List而已。selectList()採用代理模式,使用排程器Executor的query()方法實現。上一節著重講過Executor是SqlSession各個方法的具體實現,是mybatis執行的核心,通過排程StatementHandler ParameterHandler ResultSetHandler三個元件來完成sqlSession操作資料庫的整個過程。Executor的實現類有SimpleExecutor ReuseExecutor BatchExecutor等,它們的基類都是BaseExecutor。下面來分析BaseExecutor的query

2.2 BaseExecutor的query() 排程器開始資料庫query

// BaseExecutor的查詢方法
 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   // 從MappedStatement中找到boundSql成員變數,前面SqlSessionFactory建立部分講到過Mapper解析時的三大元件:MappedStatement SqlSource BoundSql
   // 其中BoundSql通過sql執行語句和入參,來組裝最終查詢資料庫用到的sql。
   BoundSql boundSql = ms.getBoundSql(parameter);

   // 建立CacheKey,用作快取的key,不用深入理解。
   // sql的id,邏輯分頁rowBounds的offset和limit,boundSql的sql語句均相同時(主要是動態sql的存在),也就是組裝後的SQL語句完全相同時,才認為是同一個cacheKey
   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

   // 真正的查詢語句執行處,關鍵程式碼
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

// 查詢方法
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());
    // 排程器已經close了則報錯
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }

    // flush cache, 即寫入並清空cache。之後就只能從資料庫中讀取了,這樣可以防止髒cache
    // localCache和localOutputParameterCache為BaseExecutor的成員變數,它們構成了mybatis的一級快取,也就是sqlSession級別的快取,預設是開啟的。
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }

    // 從快取或資料庫中查詢結果list
    List<E> list;
    try {
      // queryStack用來記錄當前有幾條同樣的查詢語句在同時執行,也就是併發
      queryStack++;
      // 未定義resultHandler時,先嚐試從快取中取。
      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();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // statement級別的快取,只快取id相同的sql。當所有查詢語句和延遲載入的查詢語句均執行完畢後,可清空cache。這樣可節約記憶體
        clearLocalCache();
      }
    }
    return list;
  }

排程器的query方法先從MappedStatement中獲取BoundSql,它包含了sql語句和入參物件等變數,再構造快取的key,即cacheKey。然後先嚐試從快取中取,快取未命中則直接從資料庫中查詢。最後處理延遲載入,直接從快取中取出查詢資料即可。下面我們著重分析直接從資料庫中查詢的過程,也即queryFromDatabase()方法。

// 直接從資料庫中查詢
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // 先利用佔位符將本次查詢設定到本地cache中,個人理解是防止後面延遲載入時cache為空
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    // 真正的資料庫查詢,後面詳細分析
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    // 查到了結果後,將前面的佔位符的cache刪掉
    localCache.removeObject(key);
  }

  // 將查詢結果放到本地cache中快取起來
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

doQuery()進行真正的資料庫查詢,它由SimpleExecutor等具體類來實現。我們以SimpleExecutor為例分析。

// 資料庫查詢
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,用來執行sql語句。SimpleExecutor建立的是RoutingStatementHandler。
    // 它的是一個門面類,幾乎所有方法都是通過代理來實現。代理則由配置XML settings節點的statementType區分。故僅僅是一個分發和路由。後面詳細分析
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

    // 構造Statement,後面詳細分析
    stmt = prepareStatement(handler, ms.getStatementLog());

    // 通過語句執行器的query方法進行查詢, 查詢結果通過resultHandler處理後返回。後面詳細分析
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

doQuery流程為

  1. 先建立StatementHandler語句處理器。前面講過StatementHandler是mybatis四大元件之一,負責sql語句的執行。根據XML配置檔案的settings節點的statementType子元素,來建立不同的實現類,如SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler。他們的基類統一為BaseStatementHandler,外觀類為RoutingStatementHandler(後面詳細分析)。
  2. 建立完StatementHandler後,呼叫prepareStatement進行初始化,
  3. 然後呼叫實現類的query方法進行查詢。

2.3 StatementHandler的query(), 語句處理器進行查詢

下面我們來看StatementHandler是如何執行的,先看StatementHandler的建立過程。

2.3.1 StatementHandler的建立過程

// 建立RoutingStatementHandler,它是StatementHandler的外觀類,也是StatementHandler的一個實現類
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // 直接構造一個RoutingStatementHandler
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 將statementHandler,新增為外掛的目標執行器。外掛通過配置XML檔案的plugins節點設定。
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

// RoutingStatementHandler的構造器,根據statementType變數來建立不同的StatementHandler實現,作為它的代理
// RoutingStatementHandler的幾乎所有方法都是通過這些代理實現的,典型的代理模式。
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 根據statementType建立不同的StatementHandler實現類。statementType是在xml配置檔案的settngs節點的statementType子元素中設定的。
    switch (ms.getStatementType()) {
      case STATEMENT:
        // 直接操作sql,不進行預編譯。此時直接進行字串拼接構造sql String
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        // 預設的型別。預處理,需要進行預編譯。可以使用引數替換,會將#轉換為?,再設定對應的引數的值。
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        // 執行儲存過程
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

2.3.2 StatementHandler的初始化

StatementHandler的初始化如下

// 通過事務構造sql執行語句statement,如JdbcTransaction
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;

  // 開啟資料庫連線,建立Connection物件。JdbcTransaction事務直接通過JDBC建立connection
  Connection connection = getConnection(statementLog);

  // 初始化statement並設定期相關變數,不同的StatementHandler實現不同。後面以RoutingStatementHandler為例分析
  stmt = handler.prepare(connection, transaction.getTimeout());

  // 設定parameterHandler,對於SimpleStatementHandler來說不用處理
  handler.parameterize(stmt);
  return stmt;
}

StatementHandler初始化步驟如下:

  1. 先開啟一個數據庫連線connection,
  2. 然後初始化statementHandler,
  3. 最後進行引數預處理。

先開啟資料庫連線connection,直接獲取資料來源dataSource的connection,即通過資料庫本身來開啟連線。

// JdbcTransaction和ManagedTransaction都是直接呼叫dataSource的getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

再進行初始化statementHandler,呼叫基類BaseStatementHandler的prepare方法完成

// 初始化statementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  // 典型的代理模式,不同的statementType建立不同的Statement,但這兒他們都呼叫到他們的基類BaseStatementHandler中的prepare方法
  return delegate.prepare(connection, transactionTimeout);
}

// BaseStatementHandler初始化statement並設定期相關變數,不同的StatementHandler實現不同。
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      // 初始化statement,由具體的StatementHandler來實現。比如SimpleStatementHandler通過JDBC connection的createStatement來建立
      statement = instantiateStatement(connection);

      // 設定timeout(超時時間)和fetchSize(獲取資料庫的行數)
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}

最後進行引數預處理。不同的statementHandler實現類有不同的引數預處理方式。

  1. SimpleStatementHandler不進行任何引數預處理,它的sql直接通過字串拼接而成。
  2. PreparedStatementHandler進行預處理,會將#轉換為?,然後設定對應的變數到sql String中。
// RoutingStatementHandler的parameterize方法,通過代理模式實現
public void parameterize(Statement statement) throws SQLException {
  // 又是代理模式,由具體的statementHandler實現類來實現
  delegate.parameterize(statement);
}

// SimpleStatementHandler不做引數預處理
public void parameterize(Statement statement) throws SQLException {
    // N/A
}

// PreparedStatementHandler進行引數預處理,通過parameterHandler實現
public void parameterize(Statement statement) throws SQLException {
   // parameterHandler可以由使用者通過外掛方式實現,mybatis預設為DefaultParameterHandler。這個方法我們不進行詳細分析了。
    parameterHandler.setParameters((PreparedStatement) statement);
}

2.3.3 statementHandler的query()進行資料庫查詢

建立和初始化statementHandler後,就可以呼叫它的query()方法來執行語句查詢了。先看SimpleStatementHandler的query過程。

// SimpleStatementHandler的query操作過程
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  // 獲取存放在boundSql中的sql執行語句
  String sql = boundSql.getSql();

  // 通過JDBC sql的statement,直接執行sql語句。入參在statement預編譯時進行了轉換並設定到statement中了。
  statement.execute(sql);

  // resultSetHandler處理查詢結果,並返回。這一部分十分複雜,但也體現了mybatis的設計精巧之處,可以相容很多複雜場景下的資料庫結果轉換。如資料庫列名和Java POJO屬性名不同時的對映,關聯資料庫的對映等。
  return resultSetHandler.<E>handleResultSets(statement);
}

PrepareStatementHandler的query操作過程如下

// PrepareStatementHandler的query操作過程
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  // PREPARE方式下,sql statement進行了預編譯,並注入了入參。它是一個PreparedStatement型別
  PreparedStatement ps = (PreparedStatement) statement;

  // 直接呼叫JDBC PreparedStatement的execute方法操作資料庫。大家應該對這兒很熟悉了,JDBC的操作
  ps.execute();

  // 結果集處理,後面詳細分析
  return resultSetHandler.<E> handleResultSets(ps);
}

query先從boundSql中獲取具體執行語句,然後通過JDBC的statement直接執行SQL語句。這兩步完成後,就從資料庫中查詢到了結果集了。從這兒可見,mybatis最底層還是通過JDBC來操作資料庫的。

mybatis對結果集的處理十分複雜,我們下面詳細分析。

2.4 ResultSetHandler處理資料庫結果集

通過JDBC完成資料庫的操作後,我們就拿到了原始的資料庫結果了。此時要將資料庫結果集ResultSet轉換為Java POJO。這一步通過mybatis四大元件之一的ResultSetHandler來實現。ResultSetHandler預設實現類為DefaultResultSetHandler,使用者也可以通過外掛的方式覆蓋它。外掛在xml配置檔案的plugins子節點下新增。下面詳細分析DefaultResultSetHandler的handleResultSets()方法。

// DefaultResultSetHandler通過handleResultSets處理資料庫結果集,處理後作為真正的結果返回。此處的關鍵是處理resultMap對映
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<Object>();
    int resultSetCount = 0;

    // 1 從JDBC操作資料庫後的statement中取出結果集ResultSet
   ResultSetWrapper rsw = getFirstResultSet(stmt);

    // 2 獲取resultMaps, mapper.xml中設定,並在mybatis初始化階段存入mappedStatement中。
    // resultMap定義了jdbc列到Java屬性的對映關係,可以解決列名和Java屬性名不一致,關聯資料庫對映等諸多問題。
    // 它是mybatis中比較複雜的地方,同時也大大擴充套件了mybatis的功能
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);

    // 3 一條條處理resultSet
    while (rsw != null && resultMapCount > resultSetCount) {
      // 取出一條resultMap,即結果對映
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 進行資料庫列到Java屬性的對映,後面詳細分析
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 取出下一條resultSet
      rsw = getNextResultSet(stmt);
      // 清空nestedResultObjects,即巢狀的Result結果集
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    // 4 處理巢狀的resultMap,即對映結果中的某些子屬性也需要resultMap對映時
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        // 取出父ResultMapping,用於巢狀情況
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          // 通過巢狀ResultMap的id,取出ResultMap
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);

          // 處理ResultSet,後面詳細分析
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    // 5 構造成List,將處理後的結果集返回
    return collapseSingleResultList(multipleResults);
}

handleResultSets流程如下

  1. 從JDBC操作資料庫後的statement中取出結果集ResultSet
  2. 獲取resultMaps, 它們定義了資料庫結果集到Java POJO的對映關係
  3. 一條條處理resultSet,呼叫handleResultSet做資料庫列到Java屬性的對映
  4. 處理巢狀的resultMap,即對映結果中的某些子屬性也需要resultMap對映時
  5. 構造成List,將處理後的結果集返回

這其中的關鍵是handleResultSet()方法進行資料庫列到Java屬性的對映,也是ORM關鍵所在。我們接著分析。

// 通過resultMap對映,處理資料庫結果集resultSet
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
  try {
    if (parentMapping != null) {
      // parentMapping不為空,表示處理的是巢狀resultMap中的子resultMap。handleRowValues後面詳細分析
      handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
    } else {
      // 非巢狀resultMap
      if (resultHandler == null) {
        // 使用者沒有自定義resultHandler時,採用DefaultResultHandler。並將最終的處理結果新增到multipleResults中
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        // 使用者定義了resultHandler時,採用使用者自定義的resultHandler
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
      }
    }
  } finally {
    // issue #228 (close resultsets)
    closeResultSet(rsw.getResultSet());
  }
}

handleResultSet針對巢狀resultMap和非巢狀resultMap做了分別處理,但都是呼叫的handleRowValues()方法,接著看。

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
  if (resultMap.hasNestedResultMaps()) {
    // 有巢狀resultMap時
    ensureNoRowBounds();
    checkResultHandler();
    handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
  } else {
    // 無巢狀resultMap時
    handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
  }
}

巢狀resultMap的處理比較麻煩,這兒不分析了,我們看非巢狀的,即handleRowValuesForSimpleResultMap。

// 非巢狀ResultMap的處理方法。根據resultMap一行行處理資料庫結果集到Java屬性的對映
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
    throws SQLException {
  DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();

  // mybatis的邏輯分頁規則為跳過rowBounds中offset之前的部分,取limit行數的資料
  // skipRows方法會跳過rowBounds中offset之前的部分。
  skipRows(rsw.getResultSet(), rowBounds);

  // 一行行處理資料庫結果集,直到取出的行數等於rowBounds的limit變數(邏輯分頁),或者所有行都取完了。
  while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
    // 處理resultMap中的discriminator,使用結果值來決定使用哪個結果對映。可以將不同的資料庫結果對映成不同的Java型別。此處不詳細分析了
    ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
    // 處理這一行資料, 得到對映後的Java結果
    Object rowValue = getRowValue(rsw, discriminatedResultMap);
    // 使用resultHandler處理得到的Java結果,這才是最終返回的Java屬性值。
    // 使用者可自定義resultHandler,否則使用DefaultResultHandler。
    // 使用者可使用ResultSetHandler外掛來自定義結果處理方式,此處體現了mybatis設計精巧之處
    storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
  }
}

// 邏輯分頁rowBounds。skipRows方法會跳過rowBounds中offset之前的部分
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
      if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
        rs.absolute(rowBounds.getOffset());
      }
    } else {
      for (int i = 0; i < rowBounds.getOffset(); i++) {
        rs.next();
      }
    }
 }

// 邏輯分頁rowBounds。shouldProcessMoreRows取limit條資料庫查詢結果
private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

mybatis的邏輯分頁規則為跳過rowBounds中offset之前的部分,取limit行數的資料。通過skipRows()和shouldProcessMoreRows()兩個方法共同完成這個功能。遍歷resultSet,通過getRowValue()方法處理一行行資料。最後呼叫resultHandler來處理轉換後的結果。

storeObject結果處理的程式碼如下

// 利用resultHandler處理經過resultMap對映的Java結果
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    if (parentMapping != null) {
     // 巢狀的resultMap,也就是子resultSet結果。連結到父resultSet中,由父resultMap一起處理。不詳細分析了
      linkToParents(rs, parentMapping, rowValue);
    } else {
      // 不是巢狀時,直接呼叫resultHandler進行最後的處理,後面詳細看
      callResultHandler(resultHandler, resultContext, rowValue);
    }
}

private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
  // 構建resultContext上下文,然後利用resultHandler處理。後面以DefaultResultHandler來分析
  resultContext.nextResultObject(rowValue);
  ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

// ResultHandler對對映後的結果做最後的處理
public void handleResult(ResultContext<? extends Object> context) {
  // DefaultResultHandler對經過resultMap對映後的Java結果不做任何處理,僅僅新增到list中,最後將list返回給selectList()等方法。
  list.add(context.getResultObject());
}

3 總結

mybatis操作資料庫的流程,也是它的四大元件Executor StatementHandler ParameterHandler ResultSetHandler的執行過程。其中Executor是排程器,StatementHandler為SQL語句執行器,ParameterHandler為入參執行器,ResultSetHandler為結果集對映執行器。四大元件分層合理,執行流程清晰,都有預設實現,同時使用者也可以利用plugin來覆蓋它。這些無一不體現了mybatis的靈活和設計精巧,值得我們平時設計構架時學習。

相關文章