详解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结果集的资料请关注我们其它相关文章!