——记一次学校的论文元数据统计项目中使用mybatis时发现的问题
目录
一、问题描述:
方法:项目中查询统计所有作者发表论文的总数
<resultMap type="com.zhou.pmas.meta.pojo.dto.CountsAuthor" id="CountsAuthor">
<result column="counts" property="counts" javaType="Integer"/>
<association property="author" javaType="com.zhou.pmas.meta.pojo.dto.AuthorDto">
<id column="author_id" property="id"/>
</association>
</resultMap>
<select id="getCountsAllByAuthor" resultMap="CountsAuthor">
SELECT COUNT(*) as counts, author_id as author_id
FROM paper_to_author
WHERE is_first=#{is_first}
AND is_corresponding=#{is_corresponding}
GROUP BY author_id
HAVING counts >= #{min}
ORDER BY COUNT(*)
</select>
问题反映:
当有作者counts相等时,返回的list中只会返回一个作者的id。也就是说返回的列表中每个counts都只对应一个作者!显然有问题!!!
首先手动运行sql语句发现返回行数正常(208),必然是返回结果在mybatis包装的时候出现问题,于是选择查看源码
二、源码详细解析:
1.详细源码过程:
因为在通过视频学习mybatis的时候有过一遍主干源码,我首先定位到处理返回结果的DefaultResultSetHandler中的handleResultSets(在此这个方法不详细分析)方法进行断点分析。
此时可以查看一下resultMap(存储我们重要的返回集处理规则)中的存储情况:
重点在idResultMappings这里!!!后续我们通过对比来讲述
再来看看原始sql返回行数
203行非常正确得了,也从此进一步证明了问题就出现在返回集封装上!!!
继续执行查找、我们能定位到handleResultSets调用到类内部的handleRowValues方法
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
简单从方法名分析可以发现当返回结果集需要有嵌套类型时会调用handleRowValuesForNestedResultMap(嵌套返回集行数据映射控制器)方法,符合我们定义的返回集。非常重要!!
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
Object rowValue = previousRowValue;
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
Object partialObject = nestedResultObjects.get(rowKey);
// issue #577 && #542
if (mappedStatement.isResultOrdered()) {
if (partialObject == null && rowValue != null) {
nestedResultObjects.clear();
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
} else {
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
if (partialObject == null) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
}
if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
previousRowValue = null;
} else if (rowValue != null) {
previousRowValue = rowValue;
}
}
其中有几个比较重要的对象,首先是rowValue行数据携带着我们查询出来的数据。
通过查看内容发现rowValue对应的不是sql中的一行,而是在我们定义的返回集中的一个独立元。如何理解,比方说我的返回集中的CountsAuthor作为整体可以是一个rowValue,而它所包含的嵌套类AuthorDto也是一个rowValue。
如何获得rowValue的呢?是通过方法getRowValue。而在该方法内又由于我们是带嵌套的返回集,从而调用applyNestedResultMapping方法。整理一下调用链路如下图。
具体看applyNestedResultMapping(嵌套返回集映射操作器)方法
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
boolean foundValues = false;
for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
final String nestedResultMapId = resultMapping.getNestedResultMapId();
if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
try {
final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
if (resultMapping.getColumnPrefix() == null) {
// try to fill circular reference only when columnPrefix
// is not specified for the nested result map (issue #215)
Object ancestorObject = ancestorObjects.get(nestedResultMapId);
if (ancestorObject != null) {
if (newObject) {
linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
}
continue;
}
}
final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
Object rowValue = nestedResultObjects.get(combinedKey);
boolean knownValue = rowValue != null;
instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
if (rowValue != null && !knownValue) {
linkObjects(metaObject, resultMapping, rowValue);
foundValues = true;
}
}
} catch (SQLException e) {
throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
}
}
return foundValues;
}
这里也有个重要的方法linkObjects,他将rowValue与metaObject(就是rowValue的包装类)绑定在一起。具体就是将rowValue注入到metaObject中originalObject所存储的上一个rowValue之中,也就是我这里例子的将AuthorDto注入到CountsAuthor之中。这便实现了嵌套的关系!!!
那么就此我们大概搞清楚嵌套的实现过程。那为何会出现我们数据丢失呢?
首先抽象思考,通过代码得知嵌套过程其实就是拿AuthorDto注入CountsAuthor中去,那么可以将CountsAuthor看作外壳,也就是出现了壳没有变,内部的AuthorDto后者将前者不断地覆盖,也就是出现了数据丢失!!!
通过继续的步过运行程序并监视metaObject我们发现确实如此
我们清楚地发现同一个CountsAuthor对象存储了不同的AuthorDto对象!后续也是如此!
那么这个不断进行后进前出的嵌套过程什么时候才能结束呢?
我们继续定位到 count=1 的数据全部遍历完,此时handleRowValuesForNestedResultMap方法即我们的控制器方法,会进入到执行storeObject方法。(为了方便重新粘下源码)
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
Object rowValue = previousRowValue;
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
Object partialObject = nestedResultObjects.get(rowKey);
// issue #577 && #542
if (mappedStatement.isResultOrdered()) {
if (partialObject == null && rowValue != null) {
nestedResultObjects.clear();
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
} else {
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
if (partialObject == null) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
}
if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
previousRowValue = null;
} else if (rowValue != null) {
previousRowValue = rowValue;
}
}
故名思意,此时控制方法才将数据保存在返回集中,终止了无限套壳挤数据这罪恶的一生......
那是如何才中止的呢? 我们可以看到执行这一保存指令的条件不约而同的指向了partialObject == null !!!
而partialObject是从nestedResultObjects中通过CacheKey获得的,那nestedResultObjects又是什么?
稍微观察其内容发现他就是存储了之前我们的所有rowValue指代的对象(比如第二个我们熟悉的8197)的一个键值对,而Cachekey则是通过对象携带的值算出用于缓存的!
问题关键原因
所以,当我们所查询的CountsAuthor看作我们的嵌套返回集的父类,当我们逐行获取sql数据时生成的返回集父类,由于只带有不唯一的字段(这里是counts),导致hash出来的CacheKey相同(当counts不变时),导致mybatis认为你的rowValue是不完整的(partial),即将这个父类当作partialObject!!!
2.问题解决
知道了问题原因我们就很容易解决了,我们可以给CountsAuthor添加上一个不重复的值使得他在hash后产生不唯一的CacheKey,比如我这里直接拿出author_id作他的一个属性并将其在resultmap中设在id标签内。
<resultMap type="com.zhou.pmas.meta.pojo.dto.CountsAuthor" id="CountsAuthor">
<id column="author_id" property="author_id"/>
<result column="counts" property="counts" javaType="Integer"/>
<association property="author" javaType="com.zhou.pmas.meta.pojo.dto.AuthorDto">
<id column="author_id" property="id"/>
</association>
</resultMap>
这个时候我们再进行调试
首先再看resultMap
发现idResultMappings变成只有author_id了,比对之前没有id标签时他将counts,author都算作id结果映射。结合此可见,定义resultMap时id标签的重要之一是在这种情况下将带嵌套的父类进行区分。这里可以细节对比一下修改前后的CacheKey。
修改前
-1190569490:-2229690933:com.zhou.pmas.meta.mapper.PaperMapper.CountsAuthor:counts:1
修改后
2084025152:600485090:com.zhou.pmas.meta.mapper.PaperMapper.CountsAuthor:author_id:13
可见唯一的id会被包含到CachKey中,从而使之唯一。
三、总结
1、嵌套返回集的封装过程
1、逐行获取数据库返回的行,开始进行一个返回结果的包装;
2、通过xml中规定规则获取数据对象,并为这个对象获取所需字段值(先父对象后嵌套对象);
3、如果要获取的对象是父对象,那么会通过CacheKey查找是否已经在字典中,若找到就继续使用这个对象当作父对象。如果没有找到就中止这个返回结果的包装。并开启下一个返回结果的包装,将这个对象存在一个Map字典之中;
4、如果要获取的对象是嵌套对象,那么在包装完嵌套对象后,将这个对象存在一个Map字典之中,并会将这个对象注入到父对象之中;
5、判断是否将xml中配置的数据对象获取完,如果获取完则读取下一sql返回行。
2、体会
整个封装过程中还有很多有意思的细节等待深究,比如在getRowValue方法在调用applyNestedResultMapping方法后又被applyNestedResultMapping所递归调用,其中细节再次没有进行分析
结语:
本人初次独自查看分析源码,在过程中可能有遗漏甚至错误,期盼大佬斧正。源码分析不易,希望这篇文章对您有所帮助