详解MyBatis ResultSetHandler 结果集的解析过程

目录
  • 正文
  • ResultSetHandler#handleResultSets
    • 第一部分:ResultSetWrapper
    • 第二部分:验证rsw对象
    • 第三部分:遍历rsw中的结果集
    • 第四部分:处理ResultSets标签
    • 第五部分:collapseSingleResultList
  • 总结

正文

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对结果集(ResultSet)进行了包装
   * getFirstResultSet获取了Statement中的第一个结果集对象,注:一般情况下只有一个结果集,如果调用存储过程可能就会获得多个结果集
   */
  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 && resultMapCount > 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节点
<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>
  • 用户调用selectMoreResults2这个方法。很显然selectMoreResults2的返回结果就是存储函数执行的结果,它执行了两个select语句,意味着会生成两个结果集对象,xml中select标签定义该存储函数的执行结果值对应一个映射关系就是userMap。但是两个结果集怎么映射成一个resultMap呢?我们真正想要的结果是把第二个结果集映射到userMap中的orderList属性。所以在进行第三部分进行遍历的时候,循环只会走一次,因为resultSetCount=2,resultMapCount=1,读者可以自定使用该业务代码进行断点调试。在解析第一个结果集时发现第一个结果集中没有orderList的信息。无法完成映射。所以才会走到第四部分进行结果集映射!

第五部分:collapseSingleResultList

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

private List<Object> collapseSingleResultList(List<Object> multipleResults) {
  return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}

总结

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

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

后续我会带来handleResultSet方法的解析~

更多关于MyBatis ResultSetHandler结果集的资料请关注我们其它相关文章!

(0)

相关推荐

  • Mybatis-plus配置分页插件返回统一结果集

    目录 一.MyBatisPlusConfig中配置分页插件 1. 分页实现的原理 二.统一结果集 1. 创建返回码定义类 2. 创建结果集类 三.编写分页接口 1. 先编写查询类 2. service层 3. controller层 4. 接口测试 总结 一.MyBatisPlusConfig中配置分页插件 /** * 配置分页插件 * @return page */ @Bean public PaginationInterceptor paginationInterceptor(){ Pagi

  • Mybatis实现ResultMap结果集

    ResultMap——解决属性名和字段名不一致的问题 数据库中的字段 新建一个项目,拷贝之前的,测试实体类字段不一致的情况 1.新建一个module——mybatis-03 2.新建db.properties配置文件 driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mybatis?useSSL=false&useEncoding=false&characterEncoding=UTF-8&serverTime

  • mybatis调用mysql存储过程(返回参数,单结果集,多结果集)

    目录 一.接收一个返回值 注意事项: 存储过程主要分成三类: 二.接收list结果集 三.返回多个结果集 四.第二种配置也可以 一.接收一个返回值 使用Map接收返回参数,output参数放在传入的param中 创建表 DROP TABLE IF EXISTS `demo`; CREATE TABLE `demo` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`

  • mybatis返回map结果集@MapKey使用的场景分析

    目录 mybatis返回map结果集@MapKey使用场景 使用id作为map的ke Map的value为Map,一条记录对应一个Map 使用name作为map的key mybatis使用@MapKey注解 背景和含义 具体示例 mybatis返回map结果集@MapKey使用场景 select的 resultType属性为map时: 通过MapKey指定map的key值 使用id作为map的ke @MapKey("id") Map<Long, UserInfo> getU

  • Android边框裁切的正确姿势实现示例

    目录 前言 1. 设置圆角边框 2. 使用ClipToOutline进行裁切 总结 前言 今天写什么呢,没有太好的思路,就随便写一些细节的点吧. 平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切. 1. 设置圆角边框 一般我们怎么设置圆角边框的 <shape xmlns:android="http://schemas.android.com/apk/res/android&quo

  • MyBatis实现两种查询树形数据的方法详解(嵌套结果集和递归查询)

    目录 方法一:使用嵌套结果集实现 1,准备工作 2,实现代码 方法二:使用递归查询实现 树形结构数据在开发中十分常见,比如:菜单数.组织树, 利用 MyBatis 提供嵌套查询功能可以很方便地实现这个功能需求.而其具体地实现方法又有两种,下面分别通过样例进行演示. 方法一:使用嵌套结果集实现 1,准备工作 (1)假设我们有如下一张菜单表 menu,其中子菜单通过 parendId 与父菜单的 id 进行关联: (2)对应的实体类如下: @Setter @Getter public class M

  • 详解MyBatis ResultSetHandler 结果集的解析过程

    目录 正文 ResultSetHandler#handleResultSets 第一部分:ResultSetWrapper 第二部分:验证rsw对象 第三部分:遍历rsw中的结果集 第四部分:处理ResultSets标签 第五部分:collapseSingleResultList 总结 正文 mybatis版本:3.5.12 mybatis通过Executor查询出结果后,通常返回的是一个List结构,再根据用户调用的API把List结构转为指定结构. 比如用户调用SqlSession#sele

  • 详解MyBatis XML配置解析

    MyBatis核心配置文件 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environm

  • 详解Mybatis是如何解析配置文件的

    缘起 经过前面三章的入门,我们大概了解了Mybatis的主线逻辑是什么样子的,在本章中,我们将正式进入Mybatis的源码海洋. Mybatis是如何解析xml的 构建Configuration 我们调用new SqlSessionFactoryBuilder().build()方法的最终目的就是构建 Configuration对象,那么Configuration何许人也?Configuration对象是一个配置管家, Configuration对象之中维护着所有的配置信息. Configura

  • 详解MyBatis工作原理

    一.Mybatis工作原理 Mybatis分层框架图 Mybatis工作原理图 源码分析:一般都是从helloworld入手 1.根据xml配置文件(全局配置文件mybatis-config.xml)创建一个SqlsessionFactory对象,mybatis-config.xml有数据源一些环境信息 2.sql映射文件EmployeeMapper.xml配置了每一个sql,以及sql的封装规则等. 3.将sql映射文件注册在全局配置文件中 4.写代码: 根据全局配置文件得到sqlsessio

  • 详解Mybatis拦截器安全加解密MySQL数据实战

    需求背景 公司为了通过一些金融安全指标(政策问题)和防止数据泄漏,需要对用户敏感数据进行加密,所以在公司项目中所有存储了用户信息的数据库都需要进行数据加密改造.包括Mysql.redis.mongodb.es.HBase等. 因为在项目中是使用springboot+mybatis方式连接数据库进行增删改查,并且项目是中途改造数据.所以为了不影响正常业务,打算这次改动尽量不侵入到业务代码,加上mybatis开放的各种拦截器接口,所以就以此进行改造数据. 本篇文章讲述如何在现有项目中尽量不侵入业务方

  • 详解MyBatis的getMapper()接口、resultMap标签、Alias别名、 尽量提取sql列、动态操作

    一.getMapper()接口 解析:getMapper()接口 IDept.class定义一个接口, 挂载一个没有实现的方法,特殊之处,借楼任何方法,必须和小配置中id属性是一致的 通过代理:生成接口的实现类名称,在MyBatis底层维护名称$$Dept_abc,selectDeptByNo() 相当于是一个强类型 Eg 第一步:在cn.happy.dao中定义一个接口 package cn.happy.dao; import java.util.List; import cn.happy.e

  • 详解MyBatis逆向工程

    1.什么是mybatis逆向工程 在使用mybatis时需要程序员自己编写sql语句,针对单表的sql语句量是很大的,mybatis官方提供了一种根据数据库表生成mybatis执行代码的工具,这个工具就是一个逆向工程. 逆向工程:针对数据库单表-->生成代码(mapper.xml.mapper.java.pojo..) mybatis-generator-core-1.3.2.jar-逆向工程运行所需要的jar核心 包 2.配置逆向工程的配置文件 配置文件generatorConfig.xml

  • 详解Mybatis中的 ${} 和 #{}区别与用法

    Mybatis 的Mapper.xml语句中parameterType向SQL语句传参有两种方式:#{}和${} 我们经常使用的是#{},一般解说是因为这种方式可以防止SQL注入,简单的说#{}这种方式SQL语句是经过预编译的,它是把#{}中间的参数转义成字符串,举个例子: select * from student where student_name = #{name} 预编译后,会动态解析成一个参数标记符?: select * from student where student_name

  • 详解MyBatis日志如何做到兼容所有常用的日志框架

    前言 日志,在我们开发中是一个非常重要的话题,良好的日志打印可以帮助我们快速的定位问题,可能现在我们开发用到最多的日志框架就是slf4j了,但是日志还有其他很多优秀的框架,比如:Apache Common Log,Log4j,java.util.logging等.MyBatis作为一款优秀的ORM框架,定义了一套统一的日志接口供应用层调用,而底层却利用适配器模式兼容了我们上面所列出来的常用日志框架. MyBatis日志分类 在介绍MyBatis的全局配置文件的时候,我们提到setting内有一个

  • 详解mybatis @SelectProvider 注解

    01.前言 为什么会写这篇文章, 因为在看到 MapperAnnotationBuilder 构造方法初始化时, 发现了四个从未见过的注解 public MapperAnnotationBuilder(Configuration configuration, Class<?> type) { ... sqlAnnotationTypes.add(Select.class); sqlAnnotationTypes.add(Insert.class); sqlAnnotationTypes.add

随机推荐