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方法。

Mybatis源码(四)_第1张图片

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 selectOne(String statement, Object parameter) {
    // 来到了 DefaultSqlSession
    // Popular vote was to return null on 0 results and throw exception on too many.
    List 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  List 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 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()
// 由  标签决定
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 list = (List) 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);

你可能感兴趣的:(Mybatis源码(四))