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