MyBatis(一):BaseExecutor与CachingExecutor

架构

MyBatis分为4部分

  • 动态代理(MapperProxy)
  • SQL会话(SqlSession)
  • 执行器(Executor)
  • JDBC处理器(StatementHandler)

执行过程

总体架构采用门面模式来进行,也就是SqlSession只对外提供API(CRUD,提交和关闭会话),其内部本身并不知道如何去实现的,就像餐厅的服务员一样,只知道点菜,并不知道菜怎么制作,具体的实现逻辑是交由SqlSession里面的Executor构造器来实现的

下面来研究一下从SqlSession获取Mapper去执行的时候,发生了什么

首先来看一下官网上是怎么使用MyBatis的(不使用配置文件)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样就可以调用运行了,下面就来分析一下从sqlsession开始去执行sql的过程(前面的过程其实就是设置配置文件,并且读取配置文件,以后再研究)
在这里插入图片描述
在这里插入图片描述
可以看到,这里SqlSessionFactory的实现是DefaultSessionFactory,所以看一下是怎么打开一个session的
调用的代码如下

    public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }

而openSessionFromDataSource的源码如下

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;

        DefaultSqlSession var8;
        try {
            //获取配置文件里面的环境信息(连接数据库那些信息)
            Environment environment = this.configuration.getEnvironment();
            //获取配置文件里面的TransactionFactory(事务工厂用来生成事务的)
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            //录用事务工厂去创建一个新的事务
            //(并且注意,这里的autoCommit是为false)
            //所以是不会自动提交的
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            //根据配置去获取executor,也就是执行器(sql的执行都是由他来进行的)
            //并且这里的执行器是this.configuration.getDefaultExecutorType()
            //也就是默认的Executor
            //而且注意,这个执行器是装配了新建的事务的
            Executor executor = this.configuration.newExecutor(tx, execType);
            //根据配置,executor和是否自动提交创建一个默认会话
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
            ErrorContext.instance().reset();
        }

        return var8;
    }

Executor

Executor负责进行CRUD,但其实本质上,Excutor里面只有改、查(原生的JDBC里面也只有改和查,即executeQuery和executeUpdate),同时也会涉及到一个重要的区域,也就是缓存,查会先经过缓存,如果缓存里面有,即直接取缓存,改也会经过缓存,所以Executor还要去维护缓存,当然还会有一些辅助功能,比如提交事务、关闭执行器和进行批处理

总的来说,Excutor的功能有以下几个

基本功能

  • executeQuery
  • executeUpdate
  • 缓存维护

辅助功能

  • 提交事务
  • 关闭执行器
  • 批处理刷新(可以理解成在执行多句增删改操作时,一开始只是在准备Sql,只要进行批处理刷新,这些Sql会被当成一个事务提交到MySQL处执行)

下面来看看下,ibatis的Executor有哪些实现类
在这里插入图片描述

BaseExcutor

这是一个抽象类,用来存放Executor共有功能的,比如获取连接、缓存操作(一级缓存)

那么具体是怎样的呢?

BaseExecutor有两个重要的方法

  • query
  • update

这两个方法里面都会调用交由实现类去实现的方法,分别是doQuery和doUpdate,所以在做完缓存之后,再进行实现类里面的doQuery和doUpdate方法,总的来说query和update方法的设置采用了模板设计模式

query方法

源码如下

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 (this.closed) {
        //关闭仍然调用就去抛出异常
        throw new ExecutorException("Executor was closed.");
    } else {
        //没关闭的话,判断需不需要清除一级缓存
        //也就是localCache
        //判断条件为:
        //1.第一次查询一级缓存(queryStack是一个计数器,代表该事务经过了几次这个查询,一个executor只有一个事务)
        //2.配置文件是否开启了清除一级缓存,如果开启了,进行清除(默认关闭)
        //这两个条件要都为true才会清除,所以,一级缓存的清除就会受两个条件影响
        //1.同一个会话执行多次sql
        //2.配置文件的flushCacheRequire设置为false,或默认
        if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
            this.clearLocalCache();
        }
		//下面就是查询的具体逻辑了
        List list;
        try {
            //计数器自增
            ++this.queryStack;
            //先尝试一级缓存中去获取结果
            list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
            //如果不为Null,则代表一级缓存中是有存储的(存储的是结果!!!)
            if (list != null) {
                //如果是存储过程,更新本地缓存中存储过程的参数
                this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                //如果一级缓存中没有,就查找本地数据库
                list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            //计数器又自减,变回原来值
            --this.queryStack;
        }
		//下面这部分还不知道干什么的。。。
        if (this.queryStack == 0) {
            Iterator var8 = this.deferredLoads.iterator();

            while(var8.hasNext()) {
                BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                deferredLoad.load();
            }

            this.deferredLoads.clear();
            if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                this.clearLocalCache();
            }
        }

        return list;
    }
}

在这里插入图片描述
可以看到key是怎么组成的

sessionID:方法全限定名:分页条件:sql:参数
queryFromDatabase方法

该方法就是当query方法尝试从一级缓存中取值时,如果一级缓存没有,就会走查询数据库

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //首先在本地缓存这个sql,但此时本地缓存没该sql对应的结果
    //这个动作仅仅代表有线程即将要把该key放入本地缓存
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    //下面就是进行真正的查询数据库
    try {
        //doQuery方法是由继承了BaseExecutor的方法去实现的
      	//从数据库中查找数据
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        //无论成功或失败,都要将一开始缓存的东西删掉
        //虽然hashmap对于key值重复的会出现替代
        //但如果失败了的话,本地不应该缓存这个key,是需要删除key值的!!!
      localCache.removeObject(key);
    }
    //假如查找成功,真正往一级缓存中存放数据库查出来的结果
    localCache.putObject(key, list);
    //如果执行的是存储过程,则需要把参数也记录在本地缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    //返回结果
    return list;
  }

这里就有个疑问了?为什么还没有查出结果就存入本地缓存呢?这个动作有什么必要性吗??

SimpleExecutor(默认实现)

无论执行的Sql是什么,都要进行预编译,执行一句Sql就提交一次事务

预编译是由数据库去做的

doQuery方法
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);
     //进行预编译
     //与JDBC相似,获取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 = getConnection(statementLog);
    //每次都会去预编译SQL
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
ReuseExecutor

相同的Sql不会进行预编译,执行一句Sql就提交一次事务

doQuery方法

可以看到其实doQuery方法与SimpleExecutor都挺类似的,但此时就是prepareStatement不同

 @Override
  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 stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  }
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    //查看是否已经编译过这个SQL
    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;
  }

BatchExecutor

进行批处理执行器,执行多次操作后再进行提交事务,而且需要手动去提交事务(调用doUpdate时只是在提供Sql语句但还没放在MySql执行,需要进行doFlushStatements,也就是批处理刷新功能),只针对增删改操作,读操作跟SimpleExecutor一样,必须经过预编译

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
        //读取的时候,也会去批量执行剩余的statements
        //(插入、修改才会存储起来statement,直到手动调用flushstatements才会清空)
        //所以,假如在一堆update之后,突然来了一句query,那么前面的update都会被提交上去数据库执行
      flushStatements();
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
        //获取连接
      Connection connection = getConnection(ms.getStatementLog());
        //进行预编译,获取预编译的SQL
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
        //执行
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

总结BaseExecutor

BaseExecutor总共有三种实现

  • SimpleExecutor
  • ReuseExecutor
  • BatchExecutor

重点

  • SimpleExecutor每次执行sql都会创建一个新的预处理器(PrepareStatement)
  • ReuseExecutor当执行相同的Sql时,可以重用预处理器
  • BatchExecutor是用来执行批量增删改处理的,批量去准备Sql语句,然后把批量Sql放在一个事务中进行提交,即必须执行flushStatements才会生效,而flushStatements方法是将BatchExecutor中存储的statements去执行,

缓存

前面已经提到过一级缓存是由BaseExecutor来实现的,那么二级缓存呢?

Caching Executor

Caching Executor同样实现了Executor接口,拥有二级缓存的功能,也有着Executor属性,而且使用了**装饰者模式(在不改变原有类结构和继承的情况下,通过组装这个类对象,来扩展一个新功能)**来调用一级缓存,也就是组装了BaseExecutor,也就是执行完二级缓存后,交给了下一个Executor

在这里插入图片描述
理解成就是,对BaseExecutor进行装饰,将它装进一个CachingExecutor中,添加了二级缓存功能,让BaseExecutor和CachingExecutor职责单一,BaseExecutor负责一级缓存,而CachingExecutor负责二级缓存,不破看原有的类结构,可以看到CachingExecutor装配上了Executor(以后如果看到delegate很有可能就是采用了装饰者模式,装饰者模式其实就是让类的关系不那么复杂,减少了一大堆继承关系)

二级缓存和一级缓存的区别

一级缓存是执行完语句就有,而二级缓存必须要进行事务提交才会有,因为二级缓存会进行跨线程的调用

执行缓存的顺序

先走二级缓存,再走一级缓存,二级缓存有了,就不会走一级缓存,具体逻辑在CachingExecutor的query方法里面

步骤如下

  • 判断要不要走二级缓存
    • 需要走,则进入二级缓存里找
      • 返回Null,则代表找不到,继续交由组装的另一个Executor(delegate)去实现
    • 不需要走,则交给CachingExecutor类里面组装的另一个Executor(delegate)去继续实现

SqlSession调用过程

整体的执行过程,SqlSession去调用CachingExecutor,CachingExecutor通过装饰者模式,去组装BaseExecutor的其中一个实现类,BaseExecutor是有一级缓存和获取连接操作的 ,而BaseExecutor的实现类是有预处理功能的(PrepareStatement),CachingExecutor执行了二级缓存之后,调用组装的BaseExecutor,从而实现了职责单一

会话的作用其实就是降低调用复杂性,也就是使用门面模式(服务员点菜容易,还是厨师点菜容易?)

下面来看看,使用二级缓存是怎样运行的

CachingExecutor的query方法

@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);
          //如果没有,再调用BaseExectutor去经过一级缓存去拿结果
        if (list == null) {
            //调用BaseExecutor去取结果()
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将查询出来的结果放入二级缓存中去
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
      //如果二级缓存为空,调用一级缓存的BaseExecutor的query方法(门面模式)
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

步骤如下

  • 如果二级缓存开启了
    • 从二级缓存中去取,如果没有,调用BaseExecutor去取结果(一级缓存或者访问数据库)
    • 然后将结果存储进二级缓存
  • 如果二级缓存没开启
    • 调用BaseExecutor去获取结果(一级缓存或者访问数据库)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值