mybatisソースコード分析03:エグゼキューター

注:この一連のソースコード分析は、mybatis 3.5.6、ソースコードgitee倉庫倉庫アドレス:funcy/mybatisに基づいています。

mybatissqlステートメントを実行する操作はExecutor、エグゼキューター()によって完了されます。エグゼキューターは次のmybatis3つのタイプを提供しますExecutor

タイプ 名前 関数
REUSE エグゼキュータを再利用 キャッシュされ、同じものをPreparedStatement次の実行sqlに再利用できます
BATCH バッチエグゼキュータ 変更操作をローカルに記録し、次のクエリが発生したときにプログラムが変更操作をトリガーするか、バッチで実行するのを待ちます
SIMPLE シンプルなエグゼキュータ 実行ごとに生成され、実行PreparedStatement後に閉じられ、キャッシュされません

さらに、キャッシュ関連の操作を処理するための上記の3つのエグゼキュータの装飾クラスでmybatisあるキャッシュエグゼキュータも提供されCachingExecutorます。実際の作業は、上記の3つのエグゼキュータの1つです。

Executor継続構造は次のとおりです。

1.1。BaseExecutor

BaseExecutorExecutor次のような基本的な操作を実装しました。

  • トランザクション処理:
    • commit(...):トランザクションのコミットを処理します
    • rollback(...):トランザクションのロールバックを処理します
  • キャッシュ処理:
    • createCacheKey(...):キャッシュキーを作成する
    • clearLocalCache(...):キャッシュの消去
  • カード操作:
    • query(...):クエリ操作
    • update(...):更新操作、挿入、削除もここで処理されます
  • サブクラスの実装に任せた
    • doUpdate(...):特定の更新操作は、実装するサブクラスに任されています
    • doQuery(...):特定のクエリ操作は、実装するサブクラスに任されています

次に実装に焦点を当てるときは、実装Executorするサブクラスに任されているメソッドにのみ焦点を当てます。

2.2。SimpleExecutor

SimpleExecutor実行ごとに生成され、実行PreparedStatement後に閉じられ、キャッシュされません。実装方法を見てみましょう。そのdoQuery(...)メソッドを見てみましょう。

  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 handler = configuration.newStatementHandler(wrapper, ms, parameter, 
            rowBounds, resultHandler, boundSql);
      // 得到 PrepareStatement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 关闭 Statement
      closeStatement(stmt);
    }
  }
复制代码

Statementそれを取得する方法は次のSimpleExecutor#prepareStatementとおりです。

  private Statement prepareStatement(StatementHandler handler, Log statementLog) 
        throws SQLException {
    Statement stmt;
    // 获取数据库连接
    Connection connection = getConnection(statementLog);
    // 获取 Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 处理参数设置
    handler.parameterize(stmt);
    return stmt;
  }
复制代码

このメソッドは、最初にデータベース接続を取得し、次にを取得Statementしてから、パラメーター設定を処理します。

关于数据库连接的获取,我们在分析配置文件的解析时,数据源的配置最终会转化成PooledDataSourceUnpooledDataSource对象,数据库连接就是从数据源来的。

至于Statement的生成,PreparedStatement的实例化操作方法为PreparedStatementHandler#instantiateStatement,这些都是常规的jdbc操作,就不细看了。

处理sql的执行方法为PreparedStatementHandler#query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 执行
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }
复制代码

SimpleExecutor#doQuery(...)的执行流程如下:

  1. 获取数据库连接
  2. 获取PrepareStatement
  3. 执行查询
  4. 关闭PrepareStatement

SimpleExecutor的操作就是常规的jdbc操作。

3. ReuseExecutor

ReuseExecutor会缓存PreparedStatement,下一次执行相同的sql可重用。

我们依然分析doQuery(...)方法:

  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
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    // 处理查询操作
    return handler.query(stmt, resultHandler);
  }
复制代码

SimpleExecutor相比,ReuseExecutordoQuery(...)方法并没关闭Statement.我们来看看Statement的获取操作:

private Statement prepareStatement(StatementHandler handler, Log statementLog) 
        throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 根据sql语句判断是否有Statement缓存
    if (hasStatementFor(sql)) {
      // 有缓存,直接使用
      stmt = getStatement(sql);
      applyTransactionTimeout(stmt);
    } else {
      // 没缓存,获取数据库连接,再获取 Statement
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 缓存 Statement
      putStatement(sql, stmt);
    }
    // 处理参数
    handler.parameterize(stmt);
    return stmt;
}
复制代码

可以看到,ReuseExecutor获取Statement时,会先从缓存里获取,缓存里没有才会新建一个Statement,然后将新建的Statement添加到缓存中。从这里可以看出,ReuseExecutorReuse,复用的是Statement

我们再来看看缓存Statement的结构:

public class ReuseExecutor extends BaseExecutor {

  private final Map<String, Statement> statementMap = new HashMap<>();

  ...

  private Statement getStatement(String s) {
    return statementMap.get(s);
  }

  private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
  }

}
复制代码

由些可见,缓存Statement的是一个Mapkeysql语句,valueStatement.

4. BatchExecutor

BatchExecutor会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作,即:

  1. 进行修改操作(insertupdatedelete)时,并不会立即执行,而是会缓存到本地
  2. 进行查询操作(select)时,会先处理缓存到本地的修改操作,再进行查询操作
  3. 也可行触发修改操作

从以上内容来看,这种方式似乎有大坑,列举几点如下:

  1. 修改操作缓存到本地后,如果执行前遇到意外重启,缓存的记录会不会丢失?
  2. 分布式环境下,多机共同协作,更新在A机上执行,查询在B机上执行,B机是不是不能查到B机的更新记录(B机的更新操作还在缓存中,并未执行)?

我们来看下BatchExecutor的更新操作,进入doUpdate(...)方法:

  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是当前保存的 sql,直接使用
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);// fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      // 创建连接,获取 Statement
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    // fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 保存,等待之后批量执行
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }
复制代码

BatchExecutor有成员变量会记录上一次执行的sqlMappedStatement,如果本次执行的sqlMappedStatement与上一次执行的相同,则直接使用上一次的Statement,否则就新建连接、获取Statement.

得到Statement后,会调用PreparedStatementHandler#batch方法:

  public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.addBatch();
  }
复制代码

这个方法并没有执行,只是调用PreparedStatement#addBatch方法,将当前statement保存了起来。

PreparedStatement#addBatch方法如何使用呢?简单示意下:

// 获取连接
Connection connection = getConnection();
// 预编译sql
String sql = "xxx";
PreparedStatement statement = connection.prepareStatement(sql);   

//记录1
statement.setInt(1, 1);
statement.setString(2, "one");
statement.addBatch();   

//记录2
statement.setInt(1, 2);
statement.setString(2, "two");
statement.addBatch();   

//记录3
statement.setInt(1, 3);
statement.setString(2, "three");
statement.addBatch();   

//批量执行
int[] counts = statement.executeBatch();

// 关闭statment,关闭连接
...
复制代码

BatchExecutordoUpdate(...)方法并没有执行sql语句,我们再来看看doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 处理缓存中的 statements
      flushStatements();
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, 
            rowBounds, resultHandler, boundSql);
      // 获取连接,获取Statement,处理参数
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 关闭 Statement
      closeStatement(stmt);
    }
  }
复制代码

doQuery(...)方法会先调用flushStatements()方法,然后再处理查询操作,整个过程基本同SimpleExecutor一致,即"获取数据库连接-获取Statement-处理查询-关闭Statement"等几步。我们重点来看flushStatements()方法的流程.

flushStatements()方法最终调用的是BatchExecutor#doFlushStatements方法,代码如下:

  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      if (isRollback) {
        return Collections.emptyList();
      }
      // 遍历的statementList,statementList就是缓存statement的结构
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          // 关键代码:stmt.executeBatch(),批量执行sql
          batchResult.setUpdateCounts(stmt.executeBatch());
          ...
        } catch (BatchUpdateException e) {
          ...
        }
        results.add(batchResult);
      }
      return results;
    } finally {
      ...
    }
  }
复制代码

BatchExecutor#doFlushStatements方法的关键代码就是batchResult.setUpdateCounts(stmt.executeBatch());了 ,其中的stmt.executeBatch()就是批量执行更新操作了。

上記の分析から、BatchExecutor#doUpdate(...)メソッドはSQLステートメントを実行せず、SQLステートメントを変換しStatementてキャッシュするだけであることがわかります。メソッドを実行するときはBatchExecutor#doQuery(...)、最初Statementにキャッシュが実行され、次にクエリ操作が実行されます。もちろん、メソッドを手動で呼び出しBatchExecutor#flushStatementsてキャッシュを実行することもできStatementます。

5.5。CachingExecutor

CachingExecutor上記の3種類のエグゼキュータとは異なり、キャッシュからデータを取得できる装飾クラスであり、実際の作業は上記の3種類のエグゼキュータの1つです。

public class CachingExecutor implements Executor {

  // 具体的执行器
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  ...
}
复制代码

コードの観点からは、これはのサブクラスであり、コンストラクターによって型が渡されるExecutorメンバー変数があります。つまり、作成時に、上記の3つのエグゼキュータの1つが渡され、メンバー変数に保存されますdelegateExecutorCachingExecutorCachingExecutordelegate

CachingExecutor方法は次のquery(...)とおりです。

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
        ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建缓存key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  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);
  }
复制代码

コードの観点から、CachingExecutorクエリを処理するときは最初にキャッシュから取得され、キャッシュに存在しない場合query(xxx)は特定のエグゼキュータのメソッドが実行されます。


この記事の元のテキストへのリンク:my.oschina.net/funcy/blog/…、著者の個人的なレベルに限定されており、テキストには必然的に間違いがあります。私を訂正することを歓迎します!オリジナリティは簡単ではありません。商用の再版の場合は、著者に許可を求めてください。非商用の再版の場合は、出典を示してください。

おすすめ

転載: juejin.im/post/7102811474061590564