mybatis學習_SqlSession的四大對象(1)

SqlSession 的四大對象是指:Executor, StatementHandler,ParameterHandler,ResultHandler對象。Mybatis通過四大對象的相互協作,完成對數據庫的操作。

這篇我們主要講解 Eexcutor對象。

我們觀察先一下SqlSession的實現類DefaultSqlSession中的一些方法。

  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      //拿到被解析過的sql聲明對象
      MappedStatement ms = configuration.getMappedStatement(statement);
      //調用執行器進行數據庫操作
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }


  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
       //調用執行器進行數據庫操作
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

可以發現,SqlSession拿到被解析過的sql聲明對象後,將其交給 Executor對象進行處理。

1. 繼承關係

executor見名之意就是執行sql操作的執行器。Executor的實現類有很多,具體的繼承關係如下圖

  • CachingExecutor:這是一個緩存器,在cacheEnabled配置開啓時,他會作爲Executor的一個裝飾器存在,攔截Executor的執行。mybatis的二級緩存就由它實現。
  • SimpleExecutor :普通的執行器。
  • ReuseExecutor:可重用執行器,其維護了一個Map<String, Statement>,將執行的sql作爲key,將執行的Statement作爲value保存,這樣執行相同的sql時就可以使用已經存在的Statement。
  • BatchExecutor:批處理執行器,他會將相同的操作暫存,然後進行批處理。

在默認情況下,mybatis爲我們實例化的是SimpleExecutor,也可以通過配置修改實例化的執行器類型。

2. SimpleExecutor 簡單執行器

這裏選取 SimpleExecutor的 doQuery方法來看看

 @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);
      //獲取到statment對象
      stmt = prepareStatement(handler, ms.getStatementLog());
      //調用 StatementHandler對象進行查詢操作,並得到返回結果
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    //通過Connection獲取到statment對象,並設置了一些配置參數
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

 可以看到,Executor首先獲取到了StatementHandler 對象,並調用其方法獲取到Statment對象,然後設置了一些配置參數。最後就直接調用了StatementHandler對象的query方法進行查詢操作,且直接獲取到查詢結果了。所以能發現SimpleExecutor並沒有做任何特殊的處理。

3. ReuseExecutor 重用執行器

這裏依然選取doQuery方法來看看

 public class ReuseExecutor extends BaseExecutor {

    //通過維護這個對象來實現重用
    private final Map<String, Statement> statementMap = new HashMap<>(); 

    //這個方法與SimpleExecutor的沒什麼差別,差別在 prepareStatement 方法中
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        Statement stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    }

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        BoundSql boundSql = handler.getBoundSql();
        //獲取到執行的sql語句
        String sql = boundSql.getSql();
        //在statementMap 中尋找sql對應的statment
        if (hasStatementFor(sql)) {
            stmt = getStatement(sql);
            applyTransactionTimeout(stmt);
        } else {
            //與SimpleExecutor相同,獲取到statment對象
            Connection connection = getConnection(statementLog);
            stmt = handler.prepare(connection, transaction.getTimeout());
            //將statment對象緩存到statementMap 中
            putStatement(sql, stmt);
        }
        handler.parameterize(stmt);
        return stmt;
    }
}

可以看到,ReuseExecutor將需要執行的sql語句以及其對應的statement緩存到 statementMap中,下次執行相同的sql時,就可以從緩存中直接拿到與其對應的statement對象。

4. BatchExecutor 批處理執行器

批處理執行器這用到了 jdbc中批處理功能。我們先來看看 doUpdate方法

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

public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    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;
    //如果執行的sql和statment和上一次的相同
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      //獲取到上一次的statment對象
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      //對上次的statment對象設置本次的參數
      handler.parameterize(stmt);//fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      //獲取到 Connection 和 statment對象
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      //設置參數
      handler.parameterize(stmt);    //fix Issues 322
      //這裏將 本次的sql和statment保存到全局變量中
      currentSql = sql;
      currentStatement = ms;
      //將statment保存進list
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    //添加批處理 關鍵就是看這裏
    handler.batch(stmt);
    //返回一個固定的值
    return BATCH_UPDATE_RETURN_VALUE;
  }


public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //實際上就是調用了statment的addBatch方法,並沒有對sql進行執行
    ps.addBatch();
  }

這裏有三個特殊的地方需要我們關注:

  1. 方法進來時,首先會判斷當前的sql和statement對象是否和上一次執行的相同。如果相同就直接取上一次的statement對象,然後設置本次操作的參數即可。 這樣設計就是因爲在批處理的場景中,會連續出現很多相同的sql語句。對相同操作複用同一個statement對象,節省了創建statement對象的開銷。
  2. doUpdate方法封裝好statement對象後,調用了Statementhandler的batch方法,然而這個方法只是調用了 statement的addBatch方法,並沒有對sql進行執行。所以能發現,在BatchExecutor中,增刪改操作並不會立即執行,而是會將其存儲起來,等待另一個事件觸發執行。
  3. 此方法由於並沒有真實的去執行sql,所以默認返回了一個常量,這個常量並不能作爲sql語句是否執行成功的依據。

然後我們來看看 doQuery方法:

public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      //關鍵看這裏
      flushStatements();
      //下面就是標準的執行流程
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

能發現,doQuery方法在執行前,做了一次刷新操作,這個操作最終由 doFlushStatements方法完成。

public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      if (isRollback) {
        return Collections.emptyList();
      }
      //遍歷整個 statementList
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          //這裏纔是真正調用了 statment的executorBatch方法完成了批處理操作
          batchResult.setUpdateCounts(stmt.executeBatch());
          ......
          // Close statement to close cursor #1109
          closeStatement(stmt);
        } catch (BatchUpdateException e) {......}
        results.add(batchResult);
      }
      return results;
    } finally {......}
}

很明顯,上面說到的觸發批處理執行的事件就是這裏是執行查詢語句。當執行查詢語句時,BatchStatment會將之前保存的增刪改操作一併提交執行。

5. CachingExecutor 緩存器

之所以最後說這個執行器,是因爲他與上面的三個執行器並不屬於同一個類型。他永遠是作爲上面三個執行器的裝飾器的存在,並不參與sql執行的過程。

//這個是CacheExeCutor中維護的 被裝飾對象
private final Executor delegate;

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //獲取到緩存對象
    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);
  }

很明顯,在執行查詢操作時,CacheExecutor會先嚐試從緩存中獲取結果,獲取不到時,再調用被裝飾類進行查詢。

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