mybatis返回嵌套结构字典数据丢失——走进源码浅谈mybatis如何包装嵌套返回集

——记一次学校的论文元数据统计项目中使用mybatis时发现的问题


目录

一、问题描述:

        方法:项目中查询统计所有作者发表论文的总数

        问题反映:

二、源码详细解析:

1.详细源码过程:

问题关键原因

 2.问题解决

三、总结

1、嵌套返回集的封装过程

2、体会



一、问题描述:

方法:项目中查询统计所有作者发表论文的总数

    <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 &gt;= #{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,他将rowValuemetaObject(就是rowValue的包装类)绑定在一起。具体就是将rowValue注入metaObjectoriginalObject所存储的上一个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标签时他将countsauthor都算作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所递归调用,其中细节再次没有进行分析


结语:

        本人初次独自查看分析源码,在过程中可能有遗漏甚至错误,期盼大佬斧正。源码分析不易,希望这篇文章对您有所帮助

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Mybatis返回的List中嵌套Map的情况可以通过以下方式实现。首先,定义一个返回类型为List<Map>的方法。在Mapper文件中,使用select语句查询需要的数据,并将结果映射到Map中。在映射过程中,可以使用resultMap来指定每个字段的映射关系。在Java代码中,调用该方法即可获取返回的List<Map>数据。这样,就可以实现Mybatis返回List中嵌套Map的功能。 #### 引用[.reference_title] - *1* [Mybatis中传入Map嵌套List](https://blog.csdn.net/qq_36631780/article/details/106232509)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Mybatis foreach嵌套 批量insert map list数据](https://blog.csdn.net/qq_36643786/article/details/91040919)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [老油条最近工作的一些功能场景记录(mybatis-嵌套合,返回map,还有list<map> 转map)](https://blog.csdn.net/baidu_37252709/article/details/119806516)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵宝宝吃海星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值