MyBatis(技术NeiMu):核心处理层(Executor)

回顾

前面我们看了MyBatis的StatementHandler,而StatementHandler是用于跟数据库进行交互的,并且完成调用ResultSetHandler完成结果集的映射,并且StatemenHandler采用了一个装饰者模式,实际上得到的所有StatementHandler都是RoutingStatementHandler,而RoutingStatementHandler装饰了具体的StatementHandler,不过有点滑稽的是,RoutingStatementHandler并没有做任何的增强;但其实在于数据库交互之前还需要做其他操作,比如访问缓存、更新缓存\开启事务等,而负责组装这些功能的则是Executor接口

Executor

在这里插入图片描述
在这里插入图片描述
从中也可以看到了Executor分成了总共有两大实现类

  • CachingExecutor:装饰器模式,提供了二级缓存的功能
  • BaseExecutor:基本的Executor
    • SimpleExecutor:执行一般SQL的Executor
    • BatchExecutor:批量执行SQL的Executor
    • ReuseExecutor:顾名思义,可以重用的Executor

下面来看一下Executor提供了哪些方法

在这里插入图片描述

  • update:增删改操作
  • query:查询操作
  • close:关闭资源
  • flushStatements:批量执行SQL语句
  • createCacheKey:创建缓存MappedStatement的SQL需要用到的key
  • isCached:判断指定MappedStatement的SQL是不是已经缓存过了
  • clearLocalCache:用来清空一级缓存
  • deferLoad:延迟加载缓存
  • getTransaction:获取事务对象

模板方法

  • 模板方法就是将某个方法的实现步骤给延迟到子类去实现,然后不同的子类就对于该方法执行的整体流程是一样的,但里面的一些步骤、算法会延迟到子类去实现,因此不同的子类,会有不同的逻辑

  • 模板方法符合开放-封闭原则,当我们去对某个功能模块的某个步骤需要使用新算法、或者修改算法的时候,可以通过添加子类,然后对该步骤去具体实现即可,不需要去改动源代码,提高了系统的灵活性和可扩展性

BaseExecutor

在这里插入图片描述
BaseExecutor是一个抽象类,实现了Executor的大部分方法,而BaseExecutor就是一个模板

比如update、query、fushStatement方法,其实就是一个模板方法,说白了,BaseExecutor提供了与数据库交互的方法模板

比如update方法

在这里插入图片描述
在这里插入图片描述
最后调用的doUpdate是一个抽象方法,延迟到子类去实现,其他的就不一一展示了

看一下BaseExecutor的成员属性

在这里插入图片描述

  • Transaction:事务对象有,用于事物的提交、回顾和关闭
  • Executor:封装的Executor?
  • deferredLoads:延迟加载队列
  • localCache:一级缓存
  • localOutPutParameterCache:一级缓存,用于缓存输出类型的参数
  • configuration:总配置

在这里插入图片描述
从构造方法中可以看到,当BaseExecutor被创建,对应的一级缓存也会被创建,就是一个PerpetualCache而已

一级缓存

可以看到,一级缓存是一个PerpetualCache,也就是永久缓存,底层是一个HashMap,前面已经看过了,不过这里并没有像二级缓存一样采用装饰者模式去不断增强;所以,可以看出一级缓存其实就是一个简单的以HashMap作为底层的缓存结构而已

一级缓存简介
  • 一级缓存是用来减少数据库压力的,因为数据库的连接访问是珍贵的资源,很容易成为整个系统的瓶颈;所以,为了节省这些资源,采用缓存的技术来减少数据库的负担、减少重复查询数据库的操作。
  • 一级缓存是一个会话级别的缓存,也就是说,一个SqlSesson就拥有一个一级缓存,没创建一个SqlSession去访问数据库,就会开启一个一级缓存,并且一级缓存是组装在BaseExecutor中的,而一个SqlSession代表一次会话,所以一级缓存的生命周期其实就是会话的生命周期,当会话关闭,对应的一级缓存也会消亡,一次会话其实就代表连接一次数据库;所以一级缓存仅仅只是优化了一次会话中的重复查询,当新的会话继续重复查询,依然会与数据库建立连接,要想突破这个限制,还是需要程序员去主动添加、维护Redis缓存
一级缓存的管理
  • 当执行访问操作的时候,可能会使用到缓存,也就是执行query方法的时候
  • 步骤如下
    • 先创建CacheKey对象
    • 使用CacheKey对象查找一级缓存
    • 如果命中,直接从缓存中取出返回
    • 如果没命中,查询数据库,也就是使用StatementHandler去查询数据库,然后查询出ResultSet,交由ResultHandler去完成结果集映射工作,将结果对象保存到一级缓存中

所以,下面来看query方法

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      //获取绑定的SQL,并且这里注入了实参
      //后续StatementHandler就能使用BoundSql来完成实参映射
    BoundSql boundSql = ms.getBoundSql(parameter);
      //创建CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
      //调用重载的query方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
CacheKey如何生成

下面来看看CacheKey是如何生成的,对应的方法是createCacheKey

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {

    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //CacheKey对象
    CacheKey cacheKey = new CacheKey();
    //MappedStatement的ID属性:
    //也就是命名空间+MappedStatement对应的SQL结点的ID属性
    //通常这样子组合成就成了要执行的接口方法的全限定名了
    cacheKey.update(ms.getId());
    //分页的起始偏移
    cacheKey.update(rowBounds.getOffset());
    //分页的终止位置
    cacheKey.update(rowBounds.getLimit());
    //绑定的SQL,该SQL是解析完了#{}占位符的SQL,也就是#{}占位符被替换成?的SQL
    cacheKey.update(boundSql.getSql());
    //获取#{}占位符映射的实参
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    //获取基本类型转换注册中心
    //基本上这玩意是用来判断是不是基础类型的!
    //只要注册进去的类型都会视为基础类型的
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    // 遍历需要映射的参数对象
    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可以获取对象中的值,根据get方法去获取
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
          //添加映射的实参
        cacheKey.update(value);
      }
    }
    //同时还会去添加总配置环境的环境
    if (configuration.getEnvironment() != null) {
      //也就是总配置文件正在使用的Environment标签的id属性,
      //也就是数据源的ID
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

从中可以看到CacheKey的与哪些因素相关

  • 执行SQL结点的ID和命名空间,起始也就是接口方法的全限定名字
  • 分页的偏移量
  • 分页的中止位置
  • 绑定的SQL
  • 使用到的映射实参
  • 数据源的ID,也就是Environment标签的id属性

但从上面可以看出,CacheKey使用的是update方法去进行关联这些因素,下面来看看update方法是怎么关联这些因素的

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

前面我们已经看过CacheKey了,起始就是将影响因素都放在一个ArrayList中,并且去维护hashCode属性

重载的query方法

当生成了CacheKey之后,就会去调用重载的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 (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //首次查询,并且执行的SQL结点开启了flushCache属性
    //那么在首次查询的时候就会去清空缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    //进行查询
    List<E> list;
    try {
        //query查询入栈,表示查询层数
      queryStack++;
        //从一级缓存中获取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        //如果不为Null
      if (list != null) {
          //证明缓存中有值
          //处理一级缓存的入参
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
          //如果缓存中没有,调用queryFromDatabase方法去查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
        //查询完,出栈
      queryStack--;
    }
    //如果查询层数为0,进行延迟加载
    //嵌套查询的时候要用到,可前面没有分析过嵌套查询,直接跳过了。。。
    //嵌套子查询
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
        //如果一级缓存被修改为 Statement级别,也就是语句级别的
        //会清空缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    //返回结果
    return list;
  }
  • query会通过CacheKey对象去判断一级缓存中是否有对应缓存项
    • 如果有,返回缓存项
    • 如果没有,查询数据库
  • 并且在查询前,会使用一个整形来代表入栈,也就是queryStack,该queryStack会进行自增
  • 当查询完将要返回对象时,queryStack会自减
  • 但到栈顶的时候queryStack为0,此时就要进行延迟加载的对象进行加载,其实就是嵌套子查询的结果集!
  • 并且最后会判断,当前的MyBatis总配置是否将LocalCacheScope属性改为statement,如果改为statement,代表了一级缓存将会是SQL级别的,一旦执行完SQL就会被清空,所以,一级缓存的级别是有两种的,会话级别和SQL级别,默认为会话级别,SQL级别需要开启,但一级缓存的生命周期肯定是与会话的声明周期相等,但如果是SQL级别,只是一级缓存会被清空而已
update方法的细节

insert、update、delete结点都是调用update方法

@Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //先清空缓存
    clearLocalCache();
      //然后调用doUpdate
    return doUpdate(ms, parameter);
  }

从中可以看到,只要调用了update方法,一级缓存就会被全部清空,然后调用doUpdate方法,而doUpdate方法则是延迟到对应子类去实现的

下面再看看什么操作会清空一级缓存

在这里插入图片描述
在这里插入图片描述
可以看到,commit方法会清空、rollback方法也会清空

一级缓存清空的情况
  • 执行query方法,第一次查询,并且select语句开启了flushCache属性会清空

  • 执行query方法,并且MyBatis总配置文件设置了localCacheScope属性为Statement等级,也就是一级缓存的等级为SQL级别,此时每完成一次query方法,最后都会清空一级缓存

  • 执行update方法,也就是增删改操作都会清空一级缓存

  • 事务提交会清空一级缓存,也就是执行commit方法

  • 事务回滚也会清空一级缓存,也就是执行rollback方法

  • 并且注意,清空一级缓存是一级输出参数缓存(存储过程使用)和一级缓存都会被清空

事务相关操作的细节

flushStatement方法

在这里插入图片描述
flushStatement其实就是用来批量执行SQL的,将存在缓冲区的SQL一次性全部执行

事务提交

在这里插入图片描述
可以看到,事务提交之前,需要做两件事情

  • 清空两个一级缓存
  • 刷新缓存的所有Statement
  • 最后事务进行提交

回滚操作

在这里插入图片描述
回滚操作跟提交操作类似,都是要先清空一级缓存、然后刷新缓存中的Statement,最后才会执行事务回滚

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值