MyBatis原理——执行器Executor

前言

在上层接口SqlSession分析中(MyBatis原理——用户交互接口SqlSession
)可以看到,它实际上是委托Executor来执行sql的。所以本文来分析下MyBatis的这个组件。

首先看下Executor的继承体系:
在这里插入图片描述
左侧是标准的”接口-抽象类-具体类“三层结构,接口定义规范;抽象类实现公共逻辑,并提供抽象模板方法;具体子类实现模板方法,提供个性功能。这是框架中很常见的一种设计。

右侧的CachingExecutor是为了实现MyBatis的二级缓存而设计的。

下面就自上而下来看下执行器Executor的设计。

接口Executor

在这里插入图片描述

Executor定义了数据库的增删改查方法(增删改合并为update)、事务相关的commitrollback,还有和缓存相关的一些功能。

抽象类BaseExecutor

首先看下BaseExecutor具有哪些属性字段:

public abstract class BaseExecutor implements Executor {
  // 事务
  protected Transaction transaction;
  // 装饰的执行器
  protected Executor wrapper;
  // 延迟加载的内容
  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  // 一级缓存,内置于BaseExecutor
  protected PerpetualCache localCache;
  // 处理输出参数,存储过程相关,不考虑
  protected PerpetualCache localOutputParameterCache;
  // 祖传大仓库
  protected Configuration configuration;
  // 记录查询深度,比如嵌套查询
  protected int queryStack;
  private boolean closed;

总结几点:

  • 由于Executor定义了事务相关的方法,所以这里持有Transaction
  • 有个Executor类型的包装对象,因为二级缓存采用了装饰器模式,这是供二级缓存用的,不涉及二级缓存的话,一般该字段就是自身。
  • 有个延迟加载的队列,用于嵌套查询的时候,保存外层查询结果,也有点缓存的意思。
  • 一级缓存PerpetualCache,其实就是HashMap的简单再包装,仅仅只是多了个String类型的id,相当于是给缓存定义一个名字。

BaseExecutor主要就是实现了一级缓存,然后把具体增删改查方法再次下放。比如针对查询方法query的实现:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取对应的sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 缓存的key,另说。
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      // 开始一个查询,查询栈深度要+1,这里栈深度表示嵌套查询的深度,即association和collection标签。sql语句上的嵌套是直接查的
      queryStack++;
      // 先访问一级缓存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      // 如果已存在于一级缓存中
      if (list != null) {
        // 处理输出参数,该方法仅针对存储过程,普通的查询相当于是空方法。
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 没有缓存,再从数据库中查
        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;
  }
  // 从数据库中查询
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 先放一个占位符,占位符用于DeferredLoad#canLoad()方法,区分isCached()方法。可以先不考虑
    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;
  }

这里可能延迟加载相关的占位符EXECUTION_PLACEHOLDERDeferredLoad 类比较迷惑,可以暂时忽略。若再不考虑存储过程相关内容,那么BaseExecutor执行流程可以简单总结为:

  1. 先查一级缓存PerpetualCache(HashMap再包装),有则返回,没有则步骤2。
  2. 一级缓存放个占位符,然后去查询数据库,这里提供抽象模板方法doQuery
  3. 查完后,将查询结果放入一级缓存,并返回。

针对增删改的update方法更简单,因为只需要清空一级缓存即可:

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    // 省略无关信息
    // ...
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

BaseExecutor提供了四个模板方法:

  // 增删改
  protected abstract int doUpdate(MappedStatement ms, Object parameter)
      throws SQLException;
  // 刷新Statement,在commit提交的时候会调用
  protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
      throws SQLException;
  // 查询
  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;
  // 游标相关
  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;

所以到BaseExecutor还并没有真正查询数据库,具体实现是在子类中。如开头的继承体系所示,其一共有3个子类,下面逐个分析:

具体子类

SimpleExecutor

最简单,也最常用的应该就是SimpleExecutor,来看下它所实现的增删改查模板方法:

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

  @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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      // 使用完关闭Statement,这也就是SimpleExecutor和ReuseExecutor的区别
      closeStatement(stmt);
    }
  }
  
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // 通过StatementHandler创建Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 填充参数
    handler.parameterize(stmt);
    return stmt;
  }
  
  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      // 动态代理包装,实现打印日志功能
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

可以看到两个模板方法实现几乎一致,总结一下步骤:

  1. 从仓库Configuration中新建StatementHandler对象;
  2. Transaction中获取数据库连接,这里就是JDBC的Connection了,不过这里有个小插曲就是可以根据配置,利用动态代理给Connection做一个小包装,实现打印sql语句的功能;
  3. 通过StatementHandler新建一个JDBC的Statement,并进行参数填充;
  4. 最后还是调用StatementHandler的对应方法,实现了数据库查询。

可以看到SimpleExecutor更多是在调度,自身最终也没有执行sql,而是进一步交给StatementHandler去执行。不过到此,已经看到了JDBC的东西了,StatementHandler就是通过操作JDBC的Statement来完成数据库查询的。

ReuseExecutor

ReuseExecutor其实和SimpleExecutor类似,只是正如其名,它对JDBC的Statement做了一个简单的缓存:用HashMap。而SimpleExecutor是每次都新建,用完就关闭。看ReuseExecutor#prepareStatement方法的源码,注意和上边SimpleExecutor#prepareStatement的区别

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

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 缓存中有,则取出使用
    if (hasStatementFor(sql)) {
      stmt = getStatement(sql);
      applyTransactionTimeout(stmt);
    } else {
      // 缓存中没有再新建,并放入缓存
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    }
    // 填充参数
    handler.parameterize(stmt);
    return stmt;
  }
  // 简单地去缓存中查询有没有这个key对应的Statement
  private boolean hasStatementFor(String sql) {
    try {
      return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
    } catch (SQLException e) {
      return false;
    }
  }

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

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

BatchExecutor

在说BatchExecutor之前,先介绍点JDBC的相关知识。JDBC的Statement接口有个addBatch方法,维护一个命令列表,用户可以向其中添加命令,然后再调用executeBatch批量执行。比如:

PreparedStatement statement = connection.prepareStatement("INSERT INTO `student` VALUES(?, ?)");

// 第1条sql
statement.setInt(1, 1); 
statement.setString(2, "zhangsan"); 
statement.addBatch(); 
// 第2条sql
statement.setInt(1, 2); 
statement.setString(2, "lisi"); 
statement.addBatch(); 
// 第3条sql
statement.setInt(1, 3); 
statement.setString(2, "wangwu"); 
statement.addBatch(); 
// 批量执行
int [] counts = statement.executeBatch(); 
connection.commit();

也就是攒一波sql再交给数据库执行,提高效率。

BatchExecutor正是利用这一点,针对同一Mapper执行的增删改做了优化。首先看下BatchExecutor的属性字段:

public class BatchExecutor extends BaseExecutor {

  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
  // 保存Statement列表
  private final List<Statement> statementList = new ArrayList<>();
  // 保存每个Statement对应的执行信息
  private final List<BatchResult> batchResultList = new ArrayList<>();
  // 下边两个字段类似栈顶指针,标记了列表中最后一个sql的信息
  // 当前sql
  private String currentSql;
  // 当前MappedStatement
  private MappedStatement currentStatement;

在增删改的模板方法doUpdate中,BatchExecutor做了自己的个性化实现:

  @Override
  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)) {
      // 取出上次sql的Statement
      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 {
      // 若不等于上次执行的sql,则还是按照传统方式新建Statement
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    //fix Issues 322
      // 标记当前执行的sql信息
      currentSql = sql;
      currentStatement = ms;
      // 添加到列表中
      statementList.add(stmt);
      // 添加BatchResult
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 注意这里不再是handler.update,而是batch方法
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

SimpleExecutor的主要区别就是最终调用了handler.batch而不是handler.update,将sql缓存在Statement中,而且这里还对Statement做了缓存,如果连续两次都是一样的sql(即sql.equals(currentSql) && ms.equals(currentStatement),注意这里的sql可以是带?占位符的,并不是说sql完全相同)那么就取出上次的Statement用(参考上边的例子)。

在这里插入图片描述

这里提出一点个人的看法:这里的实现是仅对比上次的sql,那么如果多个sql交叉提交,就没法重用Statement了(每次sql都不一样)。如果保存为map映射,key为sql+MappedStatement,value是对应的Statement感觉更好,不过一般情况下同一sql总是在一块执行,这样也行,实现更简单。

但是这样并没有实际执行sql,BatchExecutor将执行放在了doFlushStatements模板方法中。该模板方法在父类BaseExecutorcommit中会被调用。下面是方法实现:

  @Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      if (isRollback) {
        return Collections.emptyList();
      }
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          // 这里调用executeBatch批量执行了保存的sql
          batchResult.setUpdateCounts(stmt.executeBatch());
          // 省略绑定主键处理
          // ...
          
          closeStatement(stmt);
        } catch (BatchUpdateException e) {//省略异常处理}
        
        results.add(batchResult);
      }
      return results;
    } finally {
      // 关闭所有Statement 并清空缓存信息
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

主要做的事就是:遍历Statement列表,调用executeBatch批量提交之前保存的sql给数据库执行;绑定主键信息(省略);关闭Statement;清空缓存列表信息。

Executor的创建

ExecutorConfiguration创建,方法很简单:

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 装载插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

关于CachingExecutor的内容,放到缓存中另说。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值