我的编程空间,编程开发者的网络收藏夹
学习永远不晚

MyBatisResultSetHandler结果集的解析过程

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

MyBatisResultSetHandler结果集的解析过程

mybatis版本:3.5.12

mybatis通过Executor查询出结果后,通常返回的是一个List结构,再根据用户调用的API把List结构转为指定结构。

  • 比如用户调用SqlSession#selectOne就是List中只有一条数据,如果查询得到多条数据会抛出TooManyResultsException的异常。
  • 比如用户调用SqlSession#selectMap就是遍历List中的每个元素,把这些元素转换成key-value形式的Map结构并返回
  • 或者用户自定义返回一个User对象,也会遍历List,把元素转换为指定类型的对象

mybatis中封装了一个类叫做ResultSetHandler它用来处理查询数据库得到的结果集,并把结果集解析为用户指定类型的数据。它的调用时机就是在查询玩数据库之后,调用时机如下

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  return resultSetHandler.handleResultSets(ps);
}
复制代码

第一步先获取PreparedStatement对象,第二部执行execute方法查询数据库,第三步就是使用ResultSetHandler处理结果集。接下来就来看下resultSetHandler是如何处理结果集对象的。它的逻辑在ResultSetHandler#handleResultSets方法中

ResultSetHandler#handleResultSets

ResultSetHandler是一个接口,它只有一个实现类DefaultResultSetHandler,下面是handleResultSets方法关的键代码。我把此核心逻辑代码分为了5部分,后面章节详细介绍

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  // 第一部分:用来缓存最后的返回值,每条记录处理完之后都会存入该集合中
  final List<Object> multipleResults = new ArrayList<>();
  int resultSetCount = 0;
  
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  // 第二部分
  // 1. 先处理mappedStatement中的ResultMap标签(每个XML的SQL语句都被映射成了MappedStatement对象。)
  // 每个SQL执行的返回结果有可能是多个resultMap标签共同组成的。可能是多结果集
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  // 第三部分
  while (rsw != null && resultMapCount > resultSetCount) {
    // MappedStatement中的ResultMap数量应该和 结果集的数量一致
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 处理结果集,这是该方法中最重要的步骤
    handleResultSet(rsw, resultMap, multipleResults, null);
    // 获取下一个结果集(多结果集情况)
    rsw = getNextResultSet(stmt);
    // nestedResultObjects清空该对象,该对象是一个缓存
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }
  // 第四部分
  // 2. 先处理mappedStatement中的ResultSets标签. 因为解析ResultMap的时候,可能ResultMap中包含ResultSet标签,而ResultSet标签并未解析
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
  }
  // 第五部分
  return collapseSingleResultList(multipleResults);
}
复制代码

该代码主要分为两个大逻辑:

  • 通过getFirstResultSet方法获取第一个结果集对象,然后循环ps中的结果集,处理每个结果集。每个结果集处理完后的数据存放到multipleResults这个集合中
  • 处理多结果集剩余的部分。因为用户可能使用了resultSets标签。返回2个结果集,但是在处理第一个结果集映射成用户指定类型时,需要用到第二个结果集对象,这在第一步是无法完成的。只能在第二部完成。

比如有如下存储函数:getuserand_orders

create procedure get_user_and_orders(in id int)
begin
    select * from user;
    select * from order;
END;
复制代码

该函数的业务意义是:查询所有的用户,和所有的订单。在Mapper中定义的resultMap如下

<resultMap id="userMap" type="user">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="birthday" column="birthday"/>
    <result property="password" column="password"/>
    <association property="orderList" resultSet="orders">
        <result property="name" column="name"/>
    </association>
</resultMap>
<!--resultSets的顺序不能随意放置,否则会导致结果集为空-->
<select id="selectMoreResults2" statementType="CALLABLE" resultSets="users,orders" resultMap="userMap">
    {call get_user_and_orders(1)}
</select>
复制代码

此时如果用户执行了存储函数,那么PS中的结果集会有两个,分别是users和orders。mybatis在处理结果集时发现。结果集中有两个对象,先处理第一个,第一个结果集为users,自然要映射为User对象,给User对象的orderList属性赋值时发现结果集中没有关于订单的数据,因为订单的数据在第二个结果集中。这时候就会在第二部再去处理第二个结果集。把订单的结果集数据映射到User的orderList属性中。

下面我们详细分析上面这一长串代码。

第一部分:ResultSetWrapper

首先我们来看第一部分的三行代码

final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
复制代码
  • 首先定义了一个List类型的集合multipleResults,结果集中每一条记录解析完毕后的数据都会存放到该集合中
  • 定义变量resultSetCount,它代表结果集的个数。(结果集的个数不一定等于ResultMaps的个数哦)
  • 把结果集对象封装为一个ResultSetWrapper对象。ResultSetWrapper其实就是对JDBC中的ResultSet对象做了一个封装。包装了一些元数据的信息。下面来看下ResultSetWrapper的重要结构
public class ResultSetWrapper {
  private final ResultSet resultSet;
  private final TypeHandlerRegistry typeHandlerRegistry;
  // 结果集中的列名集合
  private final List<String> columnNames = new ArrayList<>();
  // java名称集合
  private final List<String> classNames = new ArrayList<>();
  private final List<JdbcType> jdbcTypes = new ArrayList<>();
  // ResultMap标签中指定的映射(重要!)
  private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();
  // ResultMap标签中未指定的映射字段(重要!)
  private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();
}
复制代码
  • resultSet:JDBC中的结果集对象
  • TypeHandlerRegistry:类型处理器,用于JDBC和Java类型的转换
  • columnNames:结果集中的所有列名的集合
  • classNames:每一列对应的Java类型的集合
  • jdbcTypes:每一列对应的JDBC类型的结合
  • mappedColumnNamesMap:resultMap标签中显式定义的标签Map
  • unMappedColumnNamesMap:结果集中返回但resultMap标签中未定义的列会被记录在该Map中

第二部分:验证rsw对象

上面获取了rsw对象(ResultSetWrapper,后面简称rsw了)后,接下来需要验证rsw对象。第二部分的三行代码如下

List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
复制代码
  • 首先先从mappedStatement对象中获取ResultMap对象,首先mappedStatement可以理解为XML中的每个select|update|insert|delete节点都被封装成了MS对象(mappedStatement简称MS)。那么MS对象中其实就包含了每个select|update|insert|delete节点的信息。而一个节点可能会在resultMap标签上定义多个返回结果集。比如下面代码在select标签的resultMap属性中定义了两个结果集
<select id="selectMoreResults1" statementType="CALLABLE" resultMap="users,authors">
    {call get_user_and_authors(1)}
</select>
复制代码
  • 定义变量resultMapCount,它表示一个MS对象中resultMap的个数。通常它是1。我们常用的情况也是1。要注意,第一部分定义的resultSetCount变量和resultMapCount并不一定相等。比如PreparedStatement对象中有两个结果集——那么此时的resultSetCount就是2.但是xml中select标签的resultMap属性只定义了一个映射——那么此时的resultMapCount就是1

    当resultMapCount < resultSetCount的时候,就说明多个结果集对应了XML中的一个映射关系,此时就需要解析resultSet标签

  • 最后一件事是验证rsw是否合法,代码比较简单就不详细介绍了

第三部分:遍历rsw中的结果集

接下来就是要遍历rsw中的结果集对象。并把结果集中的每条记录都根据resultMap标签定义的映射关系转化为指定类型的数据。并把它加入到第一部分提到的multipleResults集合中。第三部分的代码如下

while (rsw != null &amp;&amp; resultMapCount &gt; resultSetCount) {
  ResultMap resultMap = resultMaps.get(resultSetCount);
  // 处理结果集,这是该方法中最重要的步骤
  handleResultSet(rsw, resultMap, multipleResults, null);
  // 获取下一个结果集(多结果集情况)
  rsw = getNextResultSet(stmt);
  // nestedResultObjects清空该对象,该对象是一个缓存
  cleanUpAfterHandlingResultSet();
  resultSetCount++;
}
复制代码

改代码的意思是,当rsw存在并且resultMapCount > resultSetCount时

  • 获取结果集对应的ResultMap对象
  • 调用handleResultSet方法处理结果集对象(这个方法很重要,它实际上完成了结果集中的每条记录的解析,它其中又调用了很多重要的方法,该方法后面我会单独抽出一篇文章来讲)
  • 获取下一个结果集并且空缓存对象。nestedResultObjects是解析嵌套映射中的一个缓存对象(了解即可)每次解析完一个结果集后都要清空该对象。
  • 重复上述步骤。不过一般我们都是执行单条SQL语句,所以PreparedStatement一般只有一个结果集,该循环也只会走一次。除非调用了存储函数

第四部分:处理ResultSets标签

如果在第一部分到第三部分的循环中,顺序处理完结果集对象之后,resultSetCount数量还是大于resultMapCount,那么就证明PS对象返回的是多结果集,并且多结果集值对应了一个映射关系,此时就需要解析这个ResultSets标签。它的解析流程和第三部分一样,重点就在于handleResultSet方法。下面使用一个案例来详细说明为什么会有这部分的解析。

  • 定义一个存储函数
create procedure get_user_and_orders(in id int)
begin
    select * from user;
    select * from order;
END;
复制代码
  • xml中配置调用存储函数的select节点
&lt;resultMap id="userMap" type="user"&gt;
    &lt;id property="id" column="id"/&gt;
    &lt;result property="username" column="username"/&gt;
    &lt;result property="birthday" column="birthday"/&gt;
    &lt;result property="password" column="password"/&gt;
    &lt;association property="orderList" resultSet="orders"&gt;
        &lt;result property="name" column="name"/&gt;
    &lt;/association&gt;
&lt;/resultMap&gt;
&lt;!--resultSets的顺序不能随意放置,否则会导致结果集为空--&gt;
&lt;select id="selectMoreResults2" statementType="CALLABLE" resultSets="users,orders" resultMap="userMap"&gt;
    {call get_user_and_orders(1)}
&lt;/select&gt;
复制代码
  • 用户调用selectMoreResults2这个方法。很显然selectMoreResults2的返回结果就是存储函数执行的结果,它执行了两个select语句,意味着会生成两个结果集对象,xml中select标签定义该存储函数的执行结果值对应一个映射关系就是userMap。但是两个结果集怎么映射成一个resultMap呢?我们真正想要的结果是把第二个结果集映射到userMap中的orderList属性。所以在进行第三部分进行遍历的时候,循环只会走一次,因为resultSetCount=2,resultMapCount=1,读者可以自定使用该业务代码进行断点调试。在解析第一个结果集时发现第一个结果集中没有orderList的信息。无法完成映射。所以才会走到第四部分进行结果集映射!

第五部分:collapseSingleResultList

最后一部分很简单,它只是把最后返回的结果进行判断:如果返回结果multipleResults集合大小为1,则只返回集合中的这个元素,否则返回原对象本身

private List&lt;Object&gt; collapseSingleResultList(List&lt;Object&gt; multipleResults) {
  return multipleResults.size() == 1 ? (List&lt;Object&gt;) multipleResults.get(0) : multipleResults;
}
复制代码

总结

该篇讲述了mybatis在执行完数据库后进行结果集的大致解析过程。

  • ResultSetWrapper是对JDBC中的ResultSet对象的封装
  • 结果集解析的重点在DefaultResultSetHandler#handleResultSet这个方法中
  • XML中的resultMap可以定义多个映射关系。如果多个结果集对应一个映射关系就需要第四部分(对resultSets标签的处理)

下一篇我会带来handleResultSet方法的解析~

以上就是MyBatis ResultSetHandler 结果集的解析过程的详细内容,更多关于MyBatis ResultSetHandler结果集的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

MyBatisResultSetHandler结果集的解析过程

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

MyBatisResultSetHandler结果集的解析过程

这篇文章主要介绍了MyBatisResultSetHandler结果集的解析过程
2023-02-13

MyBatis handleResultSet结果集解析过程示例

这篇文章主要为大家介绍了MyBatis handleResultSet结果集解析过程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-16

MySQL中的innerjoin和leftjoin的区别解析(小结果集驱动大结果集)

这篇文章主要介绍了MySQL中的innerjoin和leftjoin的区别解析,本文通过场景描述给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
2023-05-19

MySQL中的 inner join 和 left join的区别解析(小结果集驱动大结果集)

目录场景描述inner join 和 left join 的区别场景描述以一个场景为例:单据A:下游子表 (数据量级小)单据B:下游主表(数据量级小)单据C:中游子表(数据量级小)单据D:中游主表(数据量级小)单据E:上游子表(数据量级
2023-05-06

如何实现linq存储过程返回多条结果集

这篇文章将为大家详细讲解有关如何实现linq存储过程返回多条结果集,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。linq存储过程默认生成的代码是ISingleResult的,也就是只能返回一
2023-06-17

Node.js查询MySQL并返回结果集给客户端的全过程

nodejs最大的优势也是大家用着最为难以理解的一点,就是它的异步功能,它几乎所有的io操作都是异步的,这也就导致很多人不理解也用不习惯,下面这篇文章主要给大家介绍了关于Node.js查询MySQL并返回结果集给客户端的相关资料,需要的朋友可以参考下
2022-12-27

我们如何在 MySQL 存储过程中处理结果集?

我们可以使用游标来处理存储过程中的结果集。基本上,游标允许我们迭代查询返回的一组行并相应地处理每一行。为了演示 CURSOR 在 MySQL 存储过程中的使用,我们正在创建以下存储过程,该过程基于名为“student_info”的表的值,如
2023-10-22

mysql 存储过程 查询结果集循环处理游标使用

注意每个版本的mysq的存储过程,触发器写法都会有些许区别,注意查看官方版本,不然你网上copy的语句可能执行无效,或者不成功 官方英文说明文档 https://dev.mysql.com/doc/refman/5.7/en/cursors.html demo
mysql 存储过程 查询结果集循环处理游标使用
2017-08-10

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录