análisis de código fuente de mybatis 03: ejecutor

Nota: esta serie de análisis de código fuente se basa en mybatis 3.5.6, la dirección del almacén del almacén de gitee del código fuente: funcy/mybatis .

mybatisLa operación de ejecutar la instrucción sql la Executorcompleta el ejecutor ( ), mybatisque proporciona tres tipos Executor:

escribe nombre Función
REUSE reutilizar ejecutor En caché , el mismo se puede reutilizar PreparedStatementpara la próxima ejecuciónsql
BATCH ejecutor de lotes Registre las operaciones de modificación localmente y espere a que el programa se active o realice las operaciones de modificación en lotes cuando haya la siguiente consulta
SIMPLE ejecutor simple Generado para cada ejecución PreparedStatement, cerrado después de la ejecución, no almacenado en caché

Además, mybatistambién se proporciona un ejecutor de caché CachingExecutor, que en realidad es una clase de decoración de los tres ejecutores anteriores para manejar operaciones relacionadas con el caché. El trabajo real sigue siendo uno de los tres ejecutores anteriores.

ExecutorLa estructura de continuación es la siguiente:

1.BaseExecutor

BaseExecutorExecutorOperaciones básicas implementadas , tales como:

  • Procesamiento de transacciones:
    • commit(...): Manejar la confirmación de la transacción
    • rollback(...): Maneja la reversión de la transacción
  • Procesamiento de caché:
    • createCacheKey(...): Crear clave de caché
    • clearLocalCache(...):limpiar cache
  • operación de cuajada:
    • query(...): operación de consulta
    • update(...): Las operaciones de actualización, inserciones y eliminaciones también se manejan aquí
  • dejado a la implementación de subclases
    • doUpdate(...): La operación de actualización específica se deja a la subclase para implementar
    • doQuery(...): La operación de consulta específica se deja a la subclase para implementar

Cuando nos enfocamos en la implementación a continuación Executor, solo nos enfocamos en los métodos que se dejan a las subclases para implementar.

2.SimpleExecutor

SimpleExecutorSe generará para cada ejecución PreparedStatement, se cerrará después de la ejecución, no se almacenará en caché, veamos cómo se implementa, echemos un vistazo a sus doQuery(...)métodos:

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

StatementLa forma de conseguirlo es 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;
  }
复制代码

Este método primero obtiene la conexión de la base de datos, luego obtiene Statementy luego procesa la configuración de los parámetros.

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

Del análisis anterior, se puede ver que el BatchExecutor#doUpdate(...)método no ejecuta la instrucción SQL, sino que solo convierte la instrucción SQL Statementy la almacena en caché. Al ejecutar el BatchExecutor#doQuery(...)método, primero se ejecutará la caché Statementy luego se ejecutará la operación de consulta. Por supuesto, el método también se puede llamar manualmente BatchExecutor#flushStatementspara ejecutar el caché Statement.

5.CachingExecutor

CachingExecutorA diferencia de los tres tipos de ejecutores anteriores, es una clase decorativa que puede obtener datos del caché, y el trabajo real sigue siendo uno de los tres tipos de ejecutores anteriores:

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

  ...
}
复制代码

Desde el punto de vista del código, es Executoruna subclase de , que tiene una variable miembro delegatecuyo tipo Executorpasa el constructor. Es decir, en el momento de la creación CachingExecutor, se pasará uno de los tres ejecutores anteriores y CachingExecutorse guardará en una variable miembro delegate.

CachingExecutorEl query(...)método es el siguiente:

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

Desde el punto de vista del código, CachingExecutoral procesar una consulta, primero se obtendrá de la memoria caché, y cuando no exista en la memoria caché, query(xxx)se ejecutará el método del ejecutor específico.


El enlace al texto original de este artículo: my.oschina.net/funcy/blog/… , limitado al nivel personal del autor, inevitablemente hay errores en el texto, ¡bienvenidos a corregirme! La originalidad no es fácil. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización. Para reimpresiones no comerciales, indique la fuente.

Supongo que te gusta

Origin juejin.im/post/7102811474061590564
Recomendado
Clasificación