mybatis之Executor

Executor是MyBatis的核心接口之一,其中定义了数据库操作的基本方法。在实际应用中经常涉及的SqlSession接口的功能,都是基于Executor接口实现的。Executor接口中定义的方法如下:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  // 执行insert、update、delete等操作
  int update(MappedStatement ms, Object parameter) throws SQLException;

  // 执行 select类型的SQL语句,返回位分为结果对象列表或游标对象
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  // 批量执行SQL语句
  List<BatchResult> flushStatements() throws SQLException;

  // 提交事务
  void commit(boolean required) throws SQLException;

  // 回滚事务
  void rollback(boolean required) throws SQLException;

  // 创建缓存中用的CacheKey对象
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  // 根据CacheKey对象查找缓存
  boolean isCached(MappedStatement ms, CacheKey key);

  // 清空一级缓存
  void clearLocalCache();

  // 延迟加载一级缓存中的数据
  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  // 获取事务对象
  Transaction getTransaction();

  // 关闭Executor对象
  void close(boolean forceRollback);

  // 检测Executor是否已关闭
  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

1.BaseExecutor

BaseExecutor是一个实现了Executor接口的抽象类,它实现了Executor接口的大部分方法,其中就使用了模板方法模式。BaseExecutor主要提供了缓存管理和事务管理的基本功能,继承BaseExecutor的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别doUpdate()方法、doQuery()方法、doQueryCursor()方法、doFlushStatement()方法,其余的功能在BaseExecutor实现。

BaseExecutor各个字段的含义如下:

  // Transaction对象,实现事务的提交、回滚和关闭操作
  protected Transaction transaction;
  // 封装的Executor对象
  protected Executor wrapper;

  // 延迟加载队列
  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  // 一级缓存,用于缓存该Executor对象查询结果集映射得到的结果对象
  protected PerpetualCache localCache;
  // 一级缓存,用于缓存输出类型的参数
  protected PerpetualCache localOutputParameterCache;

  // 用来记录嵌套查询的层数
  protected int queryStack;

一级缓存

执行 select 语句查询数据库是最常用的功能,BaseExecutor.query()方法会首先创建CacheKey对象,并根据该CacheKey对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果缓存未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。query()方法的具体实现如下:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取BoundSql对象
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 创建CacheKey对象
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 调用query()的一个重载
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

这里关注BaseExecutor.createCacheKey()方法创建的CacheKey对象由哪几部分构成,createCacheKey()方法具体实现如下:

 @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    // 检测Executor是否关闭
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // 添加MappedStatement的id
    cacheKey.update(ms.getId());
    // 添加offset
    cacheKey.update(rowBounds.getOffset());
    // 添加limit
    cacheKey.update(rowBounds.getLimit());
    // 添加SQL语句
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    // 获取实参并添加到CacheKey
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    // 添加Environment的id
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

由此可知该CacheKey对象由MappedStatement的id 、对应的offset、limit、SQL语句(包含?占位符)、用户传递的实参以及Environment.id这五部分构成。

继续来看上述代码中调用的query()方法的另一重载的具体实现,该重载会根据前面创建的CacheKey对象查询一级缓存,如果缓存命中则将缓存中记录的结果对象返回,如果缓存未命中,则调用 doQuery()方法完成数据库的查询操作并得到结果对象,之后将结果对象记录到一级缓存中。具体实现如下:

@Override
  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());
    // 检测Executor是否关闭
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // 非嵌套查询,并且<select>节点配置的flushCache属性为true时,才会清空一级缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      // 增加查询层数
      queryStack++;
      // 查询一级缓存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 针对存储过程调用的处理,其功能是:在一级缓存命中时,获取缓存中保存的输出类型参数,并设置到用户传入的实参对象中
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 调用doQuery()方法完成数据库查询,并得到映射后的结果对象,
        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) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

上面介绍了BaseExecutor中缓存的第一种功能,也就是缓存结查询得到的结果对象。除此之外,一级缓存还有第二个功能:前面在分析嵌套查询时,如果一级缓存中缓存了嵌套查询的结果对象,则可以从一级缓存中直接加载该结果对象;如果一级缓存中记录的嵌套查询的结果对象并未完全加载,则可以通过DeferredLoad实现类似延迟加载的功能。

Executor中与上述功能直接相关的方法有两个,一个是isCached()方法负责检测是否缓存指定查询的结果对象,具体实现如下:

 @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    // 检测缓存中是否缓存了对应的对象
    return localCache.getObject(key) != null;
  }

另一个是 deferLoad()方法,它负责创建DeferredLoad对象并将其添加到deferredLoads集合中,具体实现如下:

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    return doQueryCursor(ms, parameter, rowBounds, boundSql);
  }

  @Override
  public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 创建DeferredLoad对象
    DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
    if (deferredLoad.canLoad()) {
      // 一级缓存中已经记录了指定查询的结果对象 直接从缓存中加载对象,并设置到外层对象中
      deferredLoad.load();
    } else {
      // 将DeferredLoad对象添加到deferredLoads队列中,待整个外层查询结束后,再加载该结果对象
      deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
    }
  }

DeferredLoad是定义在BaseExecutor中的内部类,它负责从localCache缓存中延迟加载结果对象,其字段的含义如下:

  // 外层对象对应的MetaObject对象
    private final MetaObject resultObject;
    // 延迟加载的局生名称
    private final String property;
    // 延迟加载的属性的类型
    private final Class<?> targetType;
    // 延迟加载的结果对象在一级缓存中相应的CacheKey对象
    private final CacheKey key;
    // 一级缓存
    private final PerpetualCache localCache;
    private final ObjectFactory objectFactory;
    // 负责结采对象的类型转换
    private final ResultExtractor resultExtractor;

DeferredLoad.canLoad()方法负责检测缓存项是否已经完全加载到了缓存中。首先要说明完全加载的含义BaseExecutor.queryFromDatabase()方法中,开始查询调用doQuery()方法查询数据库之前,会先在localCache中添加占位符,待查询完成之后才将真正的结果对象放到localCache中缓存,此时该缓存项才算完全加载。BaseExecutor.queryFromDatabase()方法具体实现如下:

 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在缓存中添加占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 完成数据库查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 删除占位符
      localCache.removeObject(key);
    }
    // 将真正的结果对象放入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      // 缓存输出类型的参数
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

DeferredLoad.canLoad()方法的具体实现如下:

  public boolean canLoad() {
      // 检测缓存是否存在指定的结果对象和是否为占位符
      return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
    }

DeferredLoad.load()方法负责从缓存中加载结果对象,并设置到外层对象的相应属性中,具体实现如下:

public void load() {
      @SuppressWarnings("unchecked")
      // we suppose we get back a List
      // 从缓存中查询结果对象
      List<Object> list = (List<Object>) localCache.getObject(key);
      // 将结果对象转换成指定类型
      Object value = resultExtractor.extractObjectFromList(list, targetType);
      // 设置到外层对象的对应属性
      resultObject.setValue(property, value);
    }

介绍完DeferredLoad对象之后,来看触发DeferredLoad缓存中加载结果对象的相关代码,这段代码在 BaseExecutor. query()方法中,具体实现如下:

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  	// ...
    // 延迟加载
    if (queryStack == 0) {
      // 在最外层的查询结束时,所有嵌套查询也已经完成,相关缓存项也已经完全加载,所以在这里可以触发DeferredLoad加载一级缓存中记录的嵌套查询的结果对象
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      // 加载完成后清空deferredLoads
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        // 根据localCacheScope配置决定是否清空一级缓存
        clearLocalCache();
      }
    }
    return list;
  }

BaseExecutor.queryCursor()方法的主要功能也是查询数据库,这一点与 query()方法类似,但它不会直接将结果集映射为结果对象,而是将结果集封装成Cursor对象并返回,待用户遍历Cursor时才真正完成结果集的映射操作。另外,queryCursor()方法是直接调用doQueryCursor()这个基本方法实现的,并不会像 query()方法那样使用查询一级缓存。

BaseExecutor.update()方法负责执行insert、update、delete三类SQL语句,它是调用doUpdate()模板方法实现的。在调用doUpdate()方法之前会清空缓存,因为执行SQL语句之后,数据库中的数据已经更新,一级缓存的内容与数据库中的数据可能己经不一致了,所以需要调用clearLocalCache()方法清空一级缓存中的脏数据。

@Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    // 判断当前Executor是否已经关闭
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 清楚一级缓存
    clearLocalCache();
    // 调用doUpdate()方法
    return doUpdate(ms, parameter);
  }

事务相关操作

在BatchExecutor实现中,可以缓存多条SQL语句,等待合适的时机将缓存的多条SQL语句,并发送到数据库执行 Executor.flushStatements()方法主要是针对批处理多条SQL语句的,它会调用doFlushStatements()这个基本方法处理 Executor 缓存的多条SQL语句。在BaseExecutor.commit()、rollback()等方法中都会首先调用flushStatements()方法,然后再执行相关事务操作。

BaseExecutor. flushStatements()方法的具体实现如下:

public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
    // 判断当前Executor是否已经关闭
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 调用doFlushStatements()这个基本方法,其参数isRollBack表示是否执行Executor中缓存的SQL语句,false表示执行,true表示不执行
    return doFlushStatements(isRollBack);
  }

BaseExecutor.commit()方法首先会清空一级缓存、调用flushStatements()方法,最后才根据参数决定是否真正提交事务。 commit()方法的实现如下:

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    // 清空一级缓存
    clearLocalCache();
    // 执行缓存的SQL语句,其中调用了flushStatements(false)方法
    flushStatements();
    // 根据required参数决定是否提交事务
    if (required) {
      transaction.commit();
    }
  }

BaseExecutor.rollback()方法的实现与commit()实现类似,同样会根据参数决定是否真正回滚事务 ,区别是其中调用的是flushStatements()方法的isRollBack参数为true这就会导致Executor中缓存的 SQL 语句全部被忽略,即不会被发送到数据库执行。

BaseExecutor.close()方法首先会调用rollback()方法忽略缓存的SQL语句,之后根据参数决定是否关闭底层的数据库连接。

2.SimpleExecutor

SimpleExecutor继承了BaseExecutor抽象类,它是最简单的Executor接口实现。正如前面所说,Executor使用了模板方法模式,一级缓存等固定不变的操作都封装到了BaseExecutor。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对象 ,实际返回的是RoutingStatementHandler对象
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 完成Statement的创建和初始化
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 调用StatementHandler .query()方法,执行SQL语句,并通过ResultSetHandler,完成结果集的映射
      return handler.query(stmt, resultHandler);
    } finally {
      // 关闭Statement对象
      closeStatement(stmt);
    }
  }


  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // 创建Statements对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 处理占位符
    handler.parameterize(stmt);
    return stmt;
  }

SimpleExecutor.doQueryCursor()方法、doUpdate()方法与doQuery()方法实现类似,这里不再赘述。 SimpleExecutor不提供批量处理SQL语句的功能,所以其doFlushStatements()方法直接返回空集合,不做其他任何操作。

3.ReuseExecutor

在传统的JDBC编程中,重用Statement对象是常用的一种优化手段,该优化手段可以减少SQL预编译的开销以及创建和销毁Statement对象的开销,从而提高性能。

ReuseExecutor提供了Statement重用的功能,ReuseExecutor中通过statementMap字段缓存使用过的Statement对象,key是SQL语句,value是SQL对应的Statement对象。ReuseExecutor.doQuery()、doQueryCursor()、doUpdate()方法的实现与SimpleExecutor对应方法的实现一样,区别在于其中调用的preparestatement()方法,SimpleExecutor每次都会通过JDBC Connection创建新的Statement对象,而ReuseExecutor会先尝试重用StaternentMap缓存的Statement对象。

ReuseExecutor. prepareStatement()方法的具体实现如下:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 获取BoundSql对象
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 判断该SQL是否缓存过Statement
    if (hasStatementFor(sql)) {
      // 获取缓存的Statement对象
      stmt = getStatement(sql);
      // 修改超时时间
      applyTransactionTimeout(stmt);
    } else {
      Connection connection = getConnection(statementLog);
      // 创建新的Statement对象,并缓存到statementMap集合中
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }

当事务提交或回滚、连接关闭时,都需要关闭这些缓存的Statement对象 。前面介绍了BaseExecutor.commit()、 rollback()和close()方法时提到,其中都会调用doFlushStatements()方法,所以在该方法实现关闭Statement对象的逻辑非常合适,具体实现如下:

@Override
  public List<BatchResult> doFlushStatements(boolean isRollback) {
    // 遍历statementMap集合并关闭其中的Statement对象
    for (Statement stmt : statementMap.values()) {
      closeStatement(stmt);
    }
    // 清空statementMap
    statementMap.clear();
    return Collections.emptyList();
  }

这里需要注意一下ReuseExecutor.queryCursor()方法的使用,熟悉JDBC编程的读者知道,每个Statement对象只能对应一个结果集,当多次调用queryCursor()方法执行同SQL语句时,会复用Statement对象,只有最后一个ResultSet是可用的 。而queryCursor()方法返回的Cursor对象,在用户迭代Cursor对象时,才会真正遍历结果集对象并进行映射操作,这就可能导致使用前面Cursor对象中封装的结果集关闭。

5.BatchExecutor

应用系统在执行一条SQL语句时,会将SQL语句以及相关参数通过网络发送到数据库系统。对于频繁操作数据库的应用系统来说,如果执行SQL语句就向数据库发送一次请求,很多时间会浪费在网络通信上。使用批量处理的优化方式可以在客户端缓存多条SQL语句,并在合适的时机将多条SQL语句打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。

不过有一点需要注意,在批量执行多条SQL语句时,每次向数据库发送的SQL语句条数是有上限的,如果超过这个上限,数据库会拒绝执行这些SQL语句井抛出异常 所以批量发送SQL语句的时机很重要。

BatchExecutor实现了批处理多条SQL语句的功能,其中核心字段的含义如下:

 // 缓存多个Statement对象其中每个Statement对象中都缓存了多条SQL语句
  private final List<Statement> statementList = new ArrayList<>();
  // 记录批处理的结果,BatchResult中通过updateCounts字段记录每个Statement执行批处理的结果
  private final List<BatchResult> batchResultList = new ArrayList<>();
  // 记录当前执行的SQL语句
  private String currentSql;
  // 记录当前执行的MappedStatement对象
  private MappedStatement currentStatement;

JDBC中的批处理只支持insert、update、delete等类型的SQL语句,不支持select类型的SQL语句,所以下面要分析的是BatchExecutor.doUpdate方法。

BatchExecutor.doUpdate()方法在添加一条SQL语句时,首先会将currentSql字段记录的SQL语句以及currentStatement字段记录的MappedStatement对象与当前添加的SQL以及MappedStatement对象进行比较,如果相同则加到同一个Statement对象等待执行,如果不同则创建新的Statement对象并将其缓存到statementList集合中等待执行。doUpdate()方法具体实现如下:

@Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    // 获取配置对象
    final Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler对象
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    // 获取SQL语句
    final String sql = boundSql.getSql();
    final Statement stmt;
    // 如果当前执行的SQL模式与上次执行的SQL模式相同且对应的MappedStatement对象相同
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      // 获取statementList中的最后一个
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      // 绑定实参
      handler.parameterize(stmt);// fix Issues 322
      // 查找对应的BatchResult对象,并记录用户传入的实参
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      // 创建新的Statement对象
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 绑定实参
      handler.parameterize(stmt);    // fix Issues 322
      // 更新currentSql、currentStatement
      currentSql = sql;
      currentStatement = ms;
      // 添加刚创建的Statement对象
      statementList.add(stmt);
      // 添加新的BatchResult对象
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 底层通过调用Statement.addBatch()方法添SQL语句
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

JDBC批处理功能中Statement可以添加不同模式的SQL,但是每添加一个新模式的SQL语句都会触发一次编译操作PreparedStatement中只能添加同一模式的SQL语句,只会触发一次编译操作,但是可以通过绑定多组不同的实参实现批处理。通过上面对doUpdate()方法的分析可知,BatchExecutor会将连续添加的、相同模式的SQL语句添加到同一个Statement/PreparedStatement对象中,这样可以有效地减少编译操作的次数。

在添加完待执行的SQL语句之后来看一下BatchExecutor.doFlushStatemnts()方法是如何批量处理这些SQL语句的:

@Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      // 用于储存批量处理的结果
      List<BatchResult> results = new ArrayList<>();
      // 如果明确指定了要回滚事务,则直接返回空集合,忽略statementList集合中记录的SQL语句
      if (isRollback) {
        return Collections.emptyList();
      }
      // 遍历statementList集合
      for (int i = 0, n = statementList.size(); i < n; i++) {
        // 获取Statement对象
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        // /获取对应的BatchResult对象
        BatchResult batchResult = batchResultList.get(i);
        try {
          // 调用Statement.executeBatch()方法批量执行其中记录的SQL语句,并使用返回的int数组
          // 更新BatchResult.updateCounts字段,其中每一个元素都表示一条SQL语句影响的记录条数
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
          List<Object> parameterObjects = batchResult.getParameterObjects();
          // 获取配置的KeyGenerator对象
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            // 获取数据库生成的主键,并设置到parameterObjects中
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
            // 对于其他类型的keyGenerator,会调用其processAfter()方法
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
          // Close statement to close cursor #1109
          // 关闭Statement对象
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        // 添加BatchResult到results集合
        results.add(batchResult);
      }
      return results;
    } finally {
      // 关闭Statement对象,并清空currentSql字段、清空statementList集合、清空batchResultList集合
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

BatchExecutor.doQuery和doQueryCursor()方法的实现与前面介绍的SimpleExecutor类似,主要区别就是BatchExecutor中的这两个方法在最开始都会先调用flushStatement()方法,执行缓存SQL语句,这样才能从数据库中查询到最新的数据。

6.CachingExecutor

CachingExecutor是Executor接口的装饰器,它为Executor对象增加了二级缓存的相关的功能,先来简单介绍MyBatis中的二级缓存及其依赖的相关组件。

二级缓存简介

MyBatis中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个:

(1)首先是mybatis-config.xml配置文件中的cacheEnabled配置,它是二级缓存的总开关。只有当该配置设置为true时,后面两项的配置才会有效果,cacheEnabled的默认值为 true 。具体配置如下:

 <settings>
        <setting name="cacheEnabled" value="true"/>
        
 </settings>

(2)在前面介绍映射配置文件的解析流程时提到,映射配置文件中可以配置<cache>节点或<cache-ref>节点。如果映射配置文件中配置了这两者中的任一一个节点,则表示开启了二级缓存功能。如果配置了<cache>节点,在解析时会为该映射配置文件指定的命名空间创建相应的Cache对象作为其二级缓存,默认是PerpetualCache对象,用户可以通过<cache>节点的type属性指定自定义Cache对象。如果配置了<cache-ref>节点,在解析时则不会为当前映射配置文件指定的命名 空间创建独立的Cache对象,而是认为它与<cache-ref>节点的namespace属性指定的命名空间共享同一个Cache对象。通过<cache>节点和<cache-ref>节点的配置,用户可以在命名空间的粒度上管理二级缓存的开启和关闭。

(3)最后一个配置项是<select>节点中的useCache属性,该属性表示查询操作产生的结果对象是否要保存到二级缓存中。useCache属性的默认值是true。

TransactionalCache&TransactionalCacheManager

TransactionalCache和TransactionalCacheManager是CachingExecutor依赖的两个组件。其中,TransactionalCache继承了Cache接口,主要用于保存在某个SqlSession的某个事务中需要向某个二

级缓存中添加的缓存数据。TransactionalCache中核心字段的含义如下:

 // 底层封装的二级缓存所对应的Cache对象
  private final Cache delegate;
  // 当该字段为true时,则表示当前TransactionalCache不可查询,且提交事务时会将底层Cache清空
  private boolean clearOnCommit;
  // 暂时记录添加到TransactionalCache中的数据。在事务提交时,会将其中的数据添加到二级缓存中
  private final Map<Object, Object> entriesToAddOnCommit;
  // 记录缓存未命中的CacheKey对象
  private final Set<Object> entriesMissedInCache;

TransactionalCache.putObject()方法并没有直接将结果对象记录到其封装二级缓存中,而是暂时保存在 entriesToAddOnCommit集合中,在事务提交时才会将这些结果对象从entriesToAddOnCommit集合添加到二级缓存中。 putObject()方法的具体实现如下:

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

再来看TransactionalCache.getObject()方法,它首先会查询底层的二级缓存,并将未命中的key添加到entriesMissedInCache中,之后会根据clearOnCommit字段的值决定具体的返回值,具体实现如下:

@Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      // 将未命中的key添加到entriesMissedInCache中
      entriesMissedInCache.add(key);
    }
    // issue #146
    // 根据clearOnCommit字段的值决定具体的返回值
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

TransactionalCache.clear()方法会清空entriesToAddOnCommit集合,并设置 clearOnCommi为true 。

TransactionalCache. commit()方法会根据clearOnCommit字段的值决定是否清空二级缓存,然后调用 flushPendingEntries()方法将entriesToAddOnCommit集合中记录的结果对象保存到二级缓存中,具体实现如下:

 public void commit() {
    // 在事务提交前 清空二级缓存
    if (clearOnCommit) {
      delegate.clear();
    }
    // 将entriesToAddOnCommit集合中的数据保存到二级缓存
    flushPendingEntries();
    // 重置clearOnCommit为false ,并清空entriesToAddOnCommit和entriesMissedInCache 集合
    reset();
  }
 private void flushPendingEntries() {
    // 遍历entriesToAddOnCommit集合,将其中记录的缓存项添加到二级缓存中
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    // 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

TransactionalCache.rollback()方法会将entriesMissedlnCache集合中记录的缓存项从二级缓存中删除,并清空entriesToAddOnCommit集合和entriesMissedlnCache集合,具体实现如下:

public void rollback() {
    // 将entriesMissedInCache集合中记录的缓存项从二级缓存中删除
    unlockMissedEntries();
    // 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
    reset();
  }

TransactionalCacheManager用于管理CachingExecutor使用的二级缓存对象,其中只定义了transactionalCaches字段,它的 key 是对应的CachingExecutor使用的 二级缓存对象,value是相应TransactionaCach对象,在该TransactionalCache中封装了对应二级缓存对象,也就是这里的 key。

TransactionalCacheManager的实现比较简单,下面简单介绍各个方法的功能和实现。

*clear()方法、 putObject方法、 getObject()方法:*调用指定二级缓存对应的TransactionalCache对象的对应方法,如果transactionalCaches集合中没有对应的TransactionalCache对象,则通过getTransactiona!Cache()方法创建。

  private TransactionalCache getTransactionalCache(Cache cache) {
    // 如果transactionalCaches集合中没有对应的TransactionalCache对象,则新建
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

*commit()方法、 rollback()方法:*遍历transactionalCaches集合,井调用其中各个TransactionalCache对象的相应方法。

CachingExecutor的实现

CachingExecutor.query()方法执行查询操作的步骤如下:

(1)获取BoundSql对象,创建查询语句对应的CacheKey对象。

(2)检测是否开启了二级缓存,如果没有开启二级缓存,则直接调用底层Executor对象的query()方法查询数据库。如果开启了二级缓存,则继续后面的步骤。

(3)检测查询操作是否包含输出类型的参数,如果是这种情况,则报错

(4)调用TransactionalCacheManager.getObject()方法查询二级缓存,如果二级缓存中查找到相应的结果对象,则直接将该结果对象返回。

(5)如果二级缓存没有相应的结果对象,则调用底层Executor对象的query()方法。正如前面介绍的 ,它会先查询一级缓存,一级缓存未命中时,才会查询数据库。最后还会将得到的结果对象放入TransactionalCache.entriesToAddOnCommit集合中保存。

CachingExecutor.query()方法的具体代码如下:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取BoundSql对象
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建CacheKey对象
    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) {
      // 根据<select>节点的配置,决定是否需妥清空二级缓存
      flushCacheIfRequired(ms);
      // 检测SQL节点的useCache配置以及是否使用了resultHandler配置
      if (ms.isUseCache() && resultHandler == null) {
        // 二级缓存不能保存输出类型的参数 如果查询操作调用了包含输出参数的存储过程,则报错
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 查询二级缓存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 二级缓存没有相应的结果对象,调用封装的Executor对象的query()方法
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 将查询结果保存到TransactionalCache.entriesToAddOnCommit集合中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 没有启动二级缓存,调用封装的Executor对象的query()方法
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

再看CachingExecutor.commit()和rollback()方法的实现,它们首先调用底层的Executor对象的对应方法完成提交和回滚,然后调用TransactionalCacheManager的对应方法完成对二级缓存的操作。具体实现如下:

@Override
  public void commit(boolean required) throws SQLException {
    // 调用底层的Executor提交事务
    delegate.commit(required);
    // 遍历相关TransactionalCache对象执行commit()方法
    tcm.commit();
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    try {
      // 调用底层的Executor回滚事务
      delegate.rollback(required);
    } finally {
      if (required) {
        // 遍历相关TransactionalCache对象执行rollback()方法
        tcm.rollback();
      }
    }
  }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值