前言
对mybatis的相关原理不清楚的读者可以先阅读mybatis原理分析系列的前面几篇博客。再来看这一篇博客,然后跟着步骤debug调试,一边动手调试一边看博客会更好理解。
1.概述
在上一篇博客中介绍了结果集处理的过程。当结果集映射中出现复合对象时,会触发子查询。
此时就会有个问题,子查询多次嵌套可以出现循环子查询的问题。也就是子查询循环依赖。查询A,填充属性B,需要去查询B。查询B的时候需要填充属性A,又会去查询A。
例如下面的代码
这里根据id查找博客,博客中有评论属性,会触发子查询去查找评论。评论的结果集映射中又出现了博客属性。又会去触发子查询查找博客。这样就构成了子查询循环依赖。
这篇博客的重点就在于分析mybatis是如何解决这种情况的。
2.代码分析
根据上面的xml,编写如下的测试代码:
代码乍一眼看上去很简单,不就是查询嘛。接下来跟进源码看看mybatis做了哪些事情。
代码会执行到BaseExecutor中的query方法。
首先查询栈+1 结果为1 表示这是第一层查询。
然后从一级缓存中获取,发现此时没有,就会走queryFromDatabase 查询数据库。
第一行代码很重要,也是解决子查询循环依赖的关键。此时并没有查找到结果。仅仅将缓存key和一个占位符保存在了一级缓存当中。然后执行doQuery方法。
获取配置信息,构建StatementHandler 和预编译sql在之前的博客中都介绍过了 这里就不再赘述。代码继续执行handler.query
来到了上一篇博客中讲解的处理结果集的方法handleResultSets
具体过程参考上一篇博客,此处关注handleResultSet方法
代码最终会执行到handleRowValues 来处理每一行结果 触发普通结果集映射handleRowValuesForSimpleResultMap
getRowValue将结果行转换成一个java对象。
先执行自动映射的属性填充,然后执行手动映射的属性填充,因为此时我们要看的就是评论这个属性是如何填充的。代码定位到applyPropertyMappings
此处会遍历手动配置的结果集映射,遍历到评论的映射的时候。会触发getPropertyMappingValue来获取评论的属性值。
评论映射是个复合对象,会触发子查询getNestedQueryMappingValue
下面的代码子查询的关键
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);//获取子查询的mappedStatement
final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);//准备参数
Object value = null;
if (nestedQueryParameterObject != null) {
final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);//准备动态sql
final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);//创建缓存key
final Class<?> targetType = propertyMapping.getJavaType();
if (executor.isCached(nestedQuery, key)) {//判断是否命中一级缓存
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);//延迟加载
value = DEFERRED;
} else {
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
if (propertyMapping.isLazy()) {//判断是否懒加载
lazyLoader.addLoader(property, metaResultObject, resultLoader);//添加懒加载
value = DEFERRED;
} else {
value = resultLoader.loadResult();//实时加载 获得子查询的结果
}
}
}
return value;
}
做了一些子查询的准备工作,例如获取子查询的mappedStatement 准备参数,创建缓存key。
然后判断是否命中一级缓存。如果命中了则会触发延迟加载。否则判断是否进行懒加载,如果是要懒加载的话,则为其添加懒加载。如何做的下一篇博客再介绍。
否则就是走实时加载的逻辑,去查询数据库,获取子查询的结果。
此例子,此时会走实时加载的逻辑 resultLoader.loadResult
代码又会执行到BaseExecutor中的query方法
此时查询栈计数器为2 因为是子查询,然后会从一级缓存中获取结果。如果没有则会触发queryFromDatabase
同样 和主查询一样,会先往一级缓存中放入缓存key和对应一个占位符,来解决循环依赖。我们来看看此时的一级缓存中有哪些东西
主查询(查询博客)和子查询(查询评论)的缓存key 和对应的占位符。
添加完之后执行doQuery方法
之后的逻辑和上面差不多 在处理评论的结果集的时候。会触发获取博客的属性值
同样因为此时映射中博客属性是个子查询。
代码来到getNestedQueryMappingValue
和前一次子查询结果不同的是,在一级缓存中找到了相同的缓存key。因为在主查询的时候,就将查询博客相关的缓存key放入了一级缓存当中。所以此时子查询去查询博客的时候,就不会像刚才那样去走实时加载的逻辑了。而是走延迟加载的逻辑。ecexutor.deferLoad
加入到延迟加载队列中 DeferredLoad包含了查询结果对象,属性名,缓存key等
之后就是这样处理完子查询,都放入到延迟加载的队列当中。
代码回到queryFromDatabase 从一级缓存中删去缓存key和占位符,替换成缓存key和结果集
返回结果集 到query方法
子查询结束,查询栈减1
当代码回到主查询的query方法时 此时的查询栈等于0了。执行延迟加载defferedLoad.load方法
从一级缓存中取出结果,然后填充属性
执行完延迟加载后,将延迟加载的队列清空。最后返回结果。
3.总结
子查询的循环依赖解决主要思路如下:
- 一级缓存记录下已执行过的查询
- 延迟加载队列记录下每次查询的结果
- 最后执行延迟加载