zoukankan      html  css  js  c++  java
  • Mybatis源码(四)

    四、执行SQL

    User user = mapper.selectUser(1);
    

    由于Mapper都是JDK动态代理对象,所以任意的方法都是执行触发管理类MapperProxy的invoke()方法。

    QA:

    1.引入MapperProxy为了解决什么问题?硬编码和编译时检查问题。他需要做的事情是:根据方法查找statementID的问题。
    2.进入到invoke方法的时候做了什么事情?他是怎么找到我们要执行的SQL的?
    

    invoke()方法:

    1、MapperProxy.invoke()

    1)首先判读是否需要去执行SQL,还是直接执行方法。Object本身的方法不需要去执行SQL,比如toString()、hashCode()等。

    2)获取缓存

    加入缓存是为了提升MapperMethod的获取速度。很巧妙的设计,缓存的使用在Mybatis中随处可见。

    //获取缓存,保存了方法签名和接口方法的关系
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    

    Map的computeIfAbsent()方法:根据key获取值,如果是null,z则把后面Object的值赋给key。

    java8和java9中的接口默认方法有特殊处理,返回DefaultMethodInvoker。

    普通方法返回的是PlainMethodInvoker,返回MapperMethod。

    MapperMethod中有两个主要的属性:

     // statement id 
      private final SqlCommand command;
      // 方法签名,主要是返回值的类型
      private final MethodSignature method;
    

    这两个属性都是MapperMethod的内部类。

    另外MapperMethod中定义了多个executor方法。

    2、MapperMethod.execute()

    接下来又调用了mapperMethod的execute方法:

    //SQL执行的真正起点
    mapperMethod.execute(sqlSession, args);
    

    在这一步,根据不同的type(INSERT、UPDATE、DALETE、SELECT)和返回类型。

    1)调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。

    2)调用sqlSession的insert()、update()、delete()、selectOne()方法。我们以查询为例,使用selectOne()方法。

     Object param = method.convertArgsToSqlCommandParam(args);
              // 普通 select 语句的执行入口 >>
              result = sqlSession.selectOne(command.getName(), param);
    
    3、DefaultSqlSession.selectOne()

    这里使用对外的接口默认实现类DefaultSqlSession。

    selectOne()最终也是调用了selectList()。

      @Override
      public <T> T selectOne(String statement, Object parameter) {
        // 来到了 DefaultSqlSession
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
          return list.get(0);
        } else if (list.size() > 1) {
          throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
          return null;
        }
      }
    

    在SelectList()中,我们根据command name(StatementID)从Configuration中拿到MapperedStatement。ms里面有xml中增删改查标签配置的所有属性,包含id、statementType、sqlSource、入参、出餐等。

    然后执行了Executor的query()方法。

    Executor是第二步openSession的时候创建的,创建了执行器基本类型之后,依次执行二级缓存装饰,和插件拦截。

      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          // 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    所以,如果有被插件拦截们这里会先走到插件的逻辑。如果没有显示的在settings中配置cacheEnabled=false,再走到CachingExecutor的逻辑,然后会走到BaseExecutor的query()方法。

    4、CachingExecutor.query()

    1)创建CacheKey

    QA:二级缓存的CacheKey是怎么构成的?或者说,什么样的查询才能确认是同一个查询呢?

    在BaseExecutor的createCacheKey方法中,用到了六个元素:

    cacheKey.update(ms.getId()); 
    cacheKey.update(rowBounds.getOffset()); // 0
    cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
    cacheKey.update(boundSql.getSql());
    cacheKey.update(value); // development
    cacheKey.update(configuration.getEnvironment().getId());
    

    也就是说,方法相同、翻页偏移相同、SQL相同、参数值相同、数据源环境相同,才会被认为是同一个查询。

    CacheKey的实际值举例(toString()生成的)。

    观察CacheKey的属性,里面有个List按顺序存放了这些要素。

    private static final int DEFAULT_MULTIPLIER = 37;
    private static final int DEFAULT_HASHCODE = 17;
    
    private final int multiplier;
    private int hashcode;
    private long checksum;
    private int count;
    private List<Object> updateList;
    

    如何比较两个CacheKey是否相同呢?如果一上来就是依次比较六个元素是否相同,要比较6次,效率不高。

    那有没有更好的方法呢?继承Object的每个类,都有一个hashCode()方法,用来生成哈希码。它是用来在集合中快速判重的。

    在生成CacheKey的时候(update方法),也更新了CacheKey的hashCode,它使用乘法哈希生成的(基数baseHashCode=17,乘法因子multiplier=37)。

     hashcode = multiplier * hashcode + baseHashCode;
    

    Object中的hashCode()是一个本地方法,通过随机数算法生成(OpenJDK8默认,可以通过-XX:hashCode修改)。CacheKey中的hashCode()方法进行了重写,返回自己生成的hashCode。

    QA:为什么要用37作为乘法因子呢?跟String中的31类似。

    Cachekey中的equals也进行了重写,比较CacheKey是否相同。

    @Override
      public boolean equals(Object object) {
        // 同一个对象
        if (this == object) {
          return true;
        }
        // 被比较的对象不是 CacheKey
        if (!(object instanceof CacheKey)) {
          return false;
        }
    
        final CacheKey cacheKey = (CacheKey) object;
    
        // hashcode 不相等
        if (hashcode != cacheKey.hashcode) {
          return false;
        }
        // checksum 不相等
        if (checksum != cacheKey.checksum) {
          return false;
        }
        // count 不相等
        if (count != cacheKey.count) {
          return false;
        }
    
        for (int i = 0; i < updateList.size(); i++) {
          Object thisObject = updateList.get(i);
          Object thatObject = cacheKey.updateList.get(i);
          if (!ArrayUtil.equals(thisObject, thatObject)) {
            return false;
          }
        }
        return true;
      }
    

    如果哈希值(乘法哈希)、校验值(加法哈希)、要素个数任何一个不相等,都不是同一个查询。最后才循环比较要素,防止哈希碰撞。

    CacheKey生成后,调用另一个query()方法。

    2)处理二级缓存

    首选从ms中取出cache对象,判断cache对象是否为空,如果为空,则没有查询二级缓存、写入二级缓存的流程。

    Cache cache = ms.getCache();
    // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
    // 由 <cache> 标签决定
    if (cache != null) {
        ...
    }
    

    QA:cache对象是什么时候创建的呢?

    用来解析Mapper.xml的XMLMapperBuild类,cacheElement()方法。

     // 解析 cache 属性,添加缓存对象
     cacheElement(context.evalNode("cache"));
    

    只有Mapper.xml中的标签不为空才解析。

      private void cacheElement(XNode context) {
        // 只有 cache 标签不为空才解析
        if (context != null) {
          ...
             
          builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
      }
    

    这里通过useNewCache()创建了一个Cache对象。

    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    

    QA:二级缓存为什么使用TransactionalCacheManager(TCM)来管理?

    1.首先插入一条数据(没有提交),此时二级缓存会被清空。

    2.在这个事务中查询数据,写入二级缓存。

    3.提交事务,出现异常,数据回滚。

    所以出现了数据库没有这条数据,但是二级缓存有这条数据的情况。所以Mybatis的二级缓存需要跟事务关联起来。

    QA:为什么一级缓存不需要?

    因为一个session就是一个事务,事务回滚,会发就结束了。缓存也清空了,不存在读到一级缓存中脏数据的情况。二级缓存是跨session的 ,也就是跨事务的,才可能出现对同一个方法的不同事务访问。

    1)写入二级缓存

    // 写入二级缓存
    tcm.putObject(cache, key, list); 
    

    从map中拿出TransactionalCache对象,把value添加到待提交的Map。此时缓存还没真正的写入。

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

    只有事务提交的时候缓存才真正写入。

    2)获取二级缓存

      List<E> list = (List<E>) tcm.getObject(cache, key);
    

    从map中拿出TransactionCache对象,这个对象也是对PerpetualCache经过层层装饰的缓存对象。

    再getObject(),这是一个会递归调用的方法,直到到达PerpetualCache,拿到value。

    public Object getObject(Object key) {
        return cache.get(key);
    }
    
    5、BaseExecutor.query()

    1)清空本地缓存

    queryStack用于记录查询栈,防止递归查询重复处理缓存。

    flushCache=true的时候,会先清理本地缓存。

    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        // flushCache="true"时,即使是查询,也清空一级缓存
        clearLocalCache();
    }
    

    如归没有缓存,会从数据库查询:queryFromDatabase()

      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    

    如果LocalCacheScope==STATEMENT,会清理本地缓存。

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

    2)从数据库查询

    a)缓存

    现在缓存用占位符占位。执行查询后,移除占位符,放入数据。

     // 先占位
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
    

    b)查询

    执行Executor的doQuery();默认是SimpleExecutor。

    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    
  • 相关阅读:
    异常
    动态链接库与静态链接库的区别
    OpenBLAS 安装与使用
    Eigen 优化技巧
    C++读取保存为二进制的 numpy 数组
    Sublime Text Windows版使用Conda环境
    Repeater 时间格式化
    C#与js的各种交互
    js 实现精确加减乘除
    常用正则表达式
  • 原文地址:https://www.cnblogs.com/snail-gao/p/13254728.html
Copyright © 2011-2022 走看看