美文网首页Mybatis
Mybatis 源码(四)Mybatis Excuter框架

Mybatis 源码(四)Mybatis Excuter框架

作者: xiaolyuh | 来源:发表于2019-11-06 11:18 被阅读0次

    我们在上一章介绍到,Mybatis会将所有数据库操作转换成iBatis编程模型,通过门面类SqlSession来操作数据库,但是我们深入SqlSession源码我们会发现,SqlSession啥都没干,它将数据库操作都委托给你了Excuter,如图:

    SqlSession嵌套图.png Executor接口定义.png

    Excuter框架类图

    Executor.png

    BaseExecutor

    在BaseExecutor定义了Executor的基本实现,如查询一级缓存,事务处理等不变的部分,操作数据库等变化部分由子类实现,使用了模板设计模式,下面我们来看下查询方法的源码:

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      BoundSql boundSql = ms.getBoundSql(parameter);
      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 {
      ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
      if (closed) {
        throw new ExecutorException("Executor was closed.");
      }
      if (queryStack == 0 && ms.isFlushCacheRequired()) {
        // 清空本地缓存
        clearLocalCache();
      }
      List<E> list;
      try {
        // 查询层次加一
        queryStack++;
        // 查询一级缓存
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
          // 处理存储过程的OUT参数
          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;
      // 添加缓存占位符
      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;
    }
    
    BaseExecutor查询方法流程.png

    queryFromDatabase() 方法中,我们可以看到doQuery使用的是模板方法,具体逻辑是由子类来实现的,这样做的好处是,子类只关心程序变化的部分,其他不变的部分由父类实现。提高了代码的复用性和代码的扩展性。

    SimpleExecutor

    普通的执行器,Mybatis的默认使用该执行器,每次新建Statement。我们还是来看下查询方法的源码:

    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
      Statement stmt = null;
      try {
        // 获取Mybatis配置类
        Configuration configuration = ms.getConfiguration();
        // 根据配置类获取StatementHandler
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 创建Statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
      } finally {
        closeStatement(stmt);
      }
    }
    
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;
      // 获取Connection连接
      Connection connection = getConnection(statementLog);
      // 根据Connection获取Statement
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 设置参数
      handler.parameterize(stmt);
      return stmt;
    }
    

    通过stmt = handler.prepare(connection, transaction.getTimeout());方法我们可以看出每次是新建Statement

    ReuseExecutor

    可以重用的执行器,复用的是Statement,内部以sql语句为key使用一个Map将Statement对象缓存起来,只要连接不断开,那么Statement就可以重用。

    因为每一个新的SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement的作用域是同一个SqlSession,所以其实这个缓存用处其实并不大。我们直接看下获取Statement源码,其他部分和SimpleExecutor查询方法一样。

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;
      BoundSql boundSql = handler.getBoundSql();
      String sql = boundSql.getSql();
      if (hasStatementFor(sql)) {
        // 获取复用的Statement
        stmt = getStatement(sql);
        applyTransactionTimeout(stmt);
      } else {
        // 新建Statement,并缓存
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        putStatement(sql, stmt);
      }
      handler.parameterize(stmt);
      return stmt;
    }
    
    private boolean hasStatementFor(String sql) {
      try {
        // 根据sql判断是否缓存了Statement,并判断Connection是否关闭
        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);
    }
    
    ReuseExecutor.png

    BatchExecutor

    批处理执行器,通过封装jdbc的 statement.addBatch(String sql) 以及 statement.executeBatch(); 来实现的批处理。该执行器的事务只能是手动提交模式。

    我们平时执行批量的处理是一般还可以使用sql拼接的方式。

    执行批量更新时建议一次不要更新太多数据,如果更新数据量比较大时可以分段执行。

    CachingExecutor

    如果开启了二级缓存那么Mybatis会使用CachingExecutor执行器,CachingExecutor使用了装饰器模式。

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

    通过源码我们发现,整个查询流程变成 了 L2 -> L1 -> DB。CachingExecutor的查询流程,增加了二级缓存的查询操作。

    我们在实际使用缓存过程中一般很少使用Mybatis的二级缓存,如果想做二级缓存,建议直接在service层面使用第三方缓存框架,推荐使用为监控而生的多级缓存框架 layering-cache,使用更方便灵活,查询流程是 L1 -> L2 -> DB。

    总结

    • BaseExecutor:使用了模板方法模式,定义了Executor的基本实现,它是一个抽象类,不能直接对外提供服务。
    • SimpleExecutor:普通的执行器,Mybatis的默认使用该执行器,每次新建Statement。
    • ReuseExecutor:可以重用Statement的执行器,但是这个Statement缓存只在一次SqlSession中有效,我们平时生少有在一次SqlSession中进行多次一样的查询操作,所以性能提升并不大。
    • BatchExecutor:批处理执行器
    • CachingExecutor:二级缓存执行器,使用装饰器模式,整个查询流程变成 了 L2 -> L1 -> DB。建议直接使用第三方缓存框架,如:为监控而生的多级缓存框架 layering-cache

    Mybatis 源码中文注释

    https://github.com/xiaolyuh/mybatis

    相关文章

      网友评论

        本文标题:Mybatis 源码(四)Mybatis Excuter框架

        本文链接:https://www.haomeiwen.com/subject/rirqbctx.html