mybatis source code analysis 03: executor

Note: This series of source code analysis is based on mybatis 3.5.6, the source code gitee warehouse warehouse address: funcy/mybatis .

mybatisThe operation of executing the sql statement is Executorcompleted by the executor ( ), mybatiswhich provides three types Executor:

type name Function
REUSE reuse executor Cached , the same can be reused PreparedStatementfor the next executionsql
BATCH batch executor Record the modification operations locally, and wait for the program to trigger or perform the modification operations in batches when there is the next query
SIMPLE simple executor Generated for each execution PreparedStatement, closed after execution, not cached

In addition, mybatisa cache executor is also provided CachingExecutor, which is actually a decoration class of the above three executors to handle cache-related operations. The actual work is still one of the above three executors.

ExecutorThe continuation structure is as follows:

1.BaseExecutor

BaseExecutorImplemented Executorbasic operations, such as:

  • Transaction processing:
    • commit(...): Handle the commit of the transaction
    • rollback(...): Handles the rollback of the transaction
  • Cache processing:
    • createCacheKey(...): Create cache key
    • clearLocalCache(...):clear cache
  • curd operation:
    • query(...): query operation
    • update(...): Update operations, insertions and deletions are also handled here
  • left to the implementation of subclasses
    • doUpdate(...): The specific update operation is left to the subclass to implement
    • doQuery(...): The specific query operation is left to the subclass to implement

When we focus on implementation next Executor, we only focus on methods that are left to subclasses to implement.

2.SimpleExecutor

SimpleExecutorIt will be generated for each execution PreparedStatement, closed after execution, not cached, let's see how it is implemented, let's take a look at its doQuery(...)methods:

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

StatementThe way to get it is 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;
  }
复制代码

This method first obtains the database connection, then obtains Statement, and then processes the parameter settings.

关于数据库连接的获取,我们在分析配置文件的解析时,数据源的配置最终会转化成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()就是批量执行更新操作了。

From the above analysis, it can be seen that the BatchExecutor#doUpdate(...)method does not execute the SQL statement, but only converts the SQL statement into Statementand caches it. When the BatchExecutor#doQuery(...)method is executed, the cached is executed first Statement, and then the query operation is executed. Of course, the method can also be manually called BatchExecutor#flushStatementsto execute the cache Statement.

5.CachingExecutor

CachingExecutorDifferent from the above three kinds of executors, it is a decorative class that can get data from the cache, and the actual work is still one of the above three kinds of executors:

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);
  }

  ...
}
复制代码

From the code point of view, it is Executora subclass of , which has a member variable delegatewhose type is Executorpassed in by the constructor. That is to say, at the time of creation CachingExecutor, one of the above three executors will be passed in, and CachingExecutorit will be saved to a member variable delegate.

CachingExecutorThe query(...)method is as follows:

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

From the code point of view, CachingExecutorwhen processing a query, it will be obtained from the cache first, and when it does not exist in the cache, the query(xxx)method of the specific executor will be executed.


The link to the original text of this article: my.oschina.net/funcy/blog/… , limited to the author's personal level, there are inevitably mistakes in the text, welcome to correct me! Originality is not easy. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

Guess you like

Origin juejin.im/post/7102811474061590564