【源码解读之 Mybatis】【核心篇】– 第8篇:ResultSetHandler结果集处理

第8篇:ResultSetHandler结果集处理

1. 学习目标确认

1.0 第7篇思考题解答

思考题1:DefaultParameterHandler的参数值获取为什么要设计优先级策略?

答案要点

  • 额外参数优先:foreach动态SQL生成的参数优先级最高
  • 空参数处理:避免空指针异常
  • 基本类型判断:单参数场景性能优化
  • 复杂对象反射:POJO对象通过MetaObject获取值
  • 设计优势:灵活支持多种参数类型,统一处理逻辑

思考题2:ParameterHandler如何与TypeHandler协作完成类型转换?

答案要点

  • 协作流程:获取参数值 → 选择TypeHandler → 调用setParameter()
  • 类型匹配:根据javaType和jdbcType选择TypeHandler
  • 职责分离:ParameterHandler管理参数,TypeHandler转换类型
  • TypeHandler复用:在参数设置和结果映射中都使用

思考题3:在什么情况下会产生额外参数(AdditionalParameter)?它们是如何生成和使用的?

  • foreach 动态 SQL:<foreach> 在展开时会为每次迭代生成临时参数(如 __frch_id_0__frch_id_1),通过 BoundSql.setAdditionalParameter() 写入。
  • <bind> 节点:基于 OGNL 计算表达式生成新参数,注入到执行上下文,同样体现在 BoundSql 的额外参数集。
  • 嵌套查询传参:association/collection 使用 select 属性时,会把父行的列值作为参数传递给子查询,相关值会以额外参数方式参与参数解析。
  • 使用方式:ParameterHandler 在取值时优先检查 boundSql.hasAdditionalParameter(name),命中则直接使用这些临时参数,保证动态生成的数据被正确绑定。

示例:

<select id="findByIds" resultType="User">   SELECT * FROM user WHERE id IN   <foreach collection="list" item="id" open="(" separator="," close=")">     #{id}   </foreach> </select> 

执行时会生成 __frch_id_0__frch_id_1... 并在 DefaultParameterHandler 内部以“额外参数优先”策略取值。

思考题4:如何设计一个通用的参数处理器来支持多种扩展功能(如加密、验证、日志等)?

  • 装饰器/管道化:在 DefaultParameterHandler 外层构建装饰器,分阶段执行“验证 → 转换/加密 → 记录日志 → 委托设置参数”的流水线。
  • 职责拆分:每个扩展功能独立实现(如 EncryptionParameterHandlerValidationParameterHandler),通过组合或顺序调用复用。
  • TypeHandler 协作:扩展仅在“取值”阶段介入,不破坏 TypeHandler 的类型转换职责,确保兼容性。
  • 统一启用方式:可通过插件拦截或自定义 LanguageDriver 控制何时创建自定义 ParameterHandler
  • 失败处理:验证不通过抛出明确异常;加密失败回滚为原值或抛错,避免脏数据入库。

最小实现建议:在 setParameters(ps) 前先执行校验与转换,再委托给 DefaultParameterHandler 完成最终绑定。

1.1 本篇学习目标

  1. 深入理解ResultSetHandler的设计思想和核心职责
  2. 掌握DefaultResultSetHandler的结果映射流程
  3. 理解ResultMap配置和自动映射机制
  4. 掌握嵌套查询和嵌套结果映射
  5. 了解多结果集、游标查询等高级特性

2. ResultSetHandler接口定义

/**  * 结果集处理器接口  */ public interface ResultSetHandler {     /**      * 处理查询结果集      */     <E> List<E> handleResultSets(Statement stmt) throws SQLException;        /**      * 处理游标结果集      */     <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;        /**      * 处理存储过程输出参数      */     void handleOutputParameters(CallableStatement cs) throws SQLException; } 

2.1 结果集处理流程图

sequenceDiagram participant RSH as "DefaultResultSetHandler" participant Stmt as "Statement" participant RSW as "ResultSetWrapper" participant RM as "ResultMap" participant MO as "MetaObject" participant TH as "TypeHandler" participant Caller as "List<E>" RSH->>Stmt: getResultSet() RSW->>RSH: wrap(ResultSet) loop 每个 ResultMap RSH->>RM: getResultMap(index) loop 每行数据 RSH->>MO: createResultObject() alt 自动映射 RSH->>RSW: getColumnValue() RSH->>TH: getResult(column) MO->>MO: setValue(property, value) else 手动映射 RSH->>TH: getResult(column) MO->>MO: setValue(property, value) end end RSH->>Stmt: getMoreResults() end RSH-->>Caller: 列表结果 Note over RSH: ResultMap 缓存 + TypeHandler 复用

3. DefaultResultSetHandler核心实现

3.1 处理结果集主流程

@Override public List<Object> handleResultSets(Statement stmt) throws SQLException {     final List<Object> multipleResults = new ArrayList<>();     int resultSetCount = 0;        // 获取第一个ResultSet     ResultSetWrapper rsw = getFirstResultSet(stmt);     List<ResultMap> resultMaps = mappedStatement.getResultMaps();        // 处理每个ResultSet     while (rsw != null && resultSetCount < resultMaps.size()) {         ResultMap resultMap = resultMaps.get(resultSetCount);         handleResultSet(rsw, resultMap, multipleResults, null);         rsw = getNextResultSet(stmt);         resultSetCount++;     }        return collapseSingleResultList(multipleResults); } 

3.2 处理每一行数据

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {     // 1. 创建结果对象     Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {         MetaObject metaObject = configuration.newMetaObject(rowValue);         boolean foundValues = false;              // 2. 应用自动映射         if (shouldApplyAutomaticMappings(resultMap, false)) {             foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);         }              // 3. 应用属性映射         foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;              rowValue = foundValues ? rowValue : null;     }        return rowValue; } 

3.3 与TypeHandler协作(结果取值)

  • 简单类型(如String、Integer、Long、Date等)会直接使用对应的TypeHandler从结果集中取值,无需创建目标对象。
  • 复杂类型(POJO)先创建结果对象,再通过自动映射和属性映射填充字段;嵌套关联遵循ResultMap定义。

示例:使用TypeHandler直接从ResultSet读取列值

TypeHandler<String> stringHandler = configuration.getTypeHandlerRegistry().getTypeHandler(String.class); String userName = stringHandler.getResult(rsw.getResultSet(), "user_name");  TypeHandler<Long> longHandler = configuration.getTypeHandlerRegistry().getTypeHandler(Long.class); Long userId = longHandler.getResult(rsw.getResultSet(), "user_id"); 

关键点:

  • hasTypeHandlerForResultObject(rsw, resultMap.getType())为true时,走“简单类型直取”路径;否则进入对象映射流程。
  • 结果侧与参数侧复用同一套TypeHandler体系,保证类型转换一致性。

4. ResultMap结果映射配置

4.1 基本ResultMap配置

<resultMap id="userResultMap" type="User">     <!-- ID映射 -->     <id property="id" column="user_id"/>        <!-- 普通属性映射 -->     <result property="name" column="user_name"/>     <result property="email" column="user_email"/>        <!-- 一对一关联 -->     <association property="address" javaType="Address">         <id property="id" column="addr_id"/>         <result property="street" column="street"/>     </association>        <!-- 一对多集合 -->     <collection property="orders" ofType="Order">         <id property="id" column="order_id"/>         <result property="orderNo" column="order_no"/>     </collection> </resultMap> 

4.2 自动映射机制

<settings>     <!-- 自动映射级别:NONE, PARTIAL, FULL -->     <setting name="autoMappingBehavior" value="PARTIAL"/>        <!-- 驼峰命名转换 -->     <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> 

说明:

  • NONE:仅按ResultMap显式定义映射,不做自动填充。
  • PARTIAL:对未显式映射的列做“保守自动映射”,避免覆盖已有映射;默认推荐。
  • FULL:尽可能尝试自动映射,适合字段命名规范统一的场景,但需注意覆盖风险。
  • 配合 mapUnderscoreToCamelCase=true可自动将user_name映射到userName。

5. 嵌套映射处理

5.1 嵌套查询(N+1问题)

<resultMap id="userMap" type="User">     <id property="id" column="id"/>     <result property="name" column="name"/>        <!-- 嵌套查询:会产生N+1问题 -->     <collection property="orders"                  column="id"                  select="selectOrdersByUserId"/> </resultMap> 

5.2 嵌套结果映射(解决N+1)

<resultMap id="userWithOrdersMap" type="User">     <id property="id" column="user_id"/>     <result property="name" column="user_name"/>        <!-- 嵌套结果映射:一次JOIN查询 -->     <collection property="orders" ofType="Order">         <id property="id" column="order_id"/>         <result property="orderNo" column="order_no"/>     </collection> </resultMap>  <select id="selectUserWithOrders" resultMap="userWithOrdersMap">     SELECT          u.id as user_id,         u.name as user_name,         o.id as order_id,         o.order_no as order_no     FROM t_user u     LEFT JOIN t_order o ON u.id = o.user_id     WHERE u.id = #{id} </select> 

5.3 延迟加载

<settings>     <!-- 开启延迟加载 -->     <setting name="lazyLoadingEnabled" value="true"/>     <setting name="aggressiveLazyLoading" value="false"/> </settings>  <resultMap id="userMap" type="User">     <id property="id" column="id"/>     <result property="name" column="name"/>        <!-- 延迟加载订单 -->     <collection property="orders"                  column="id"                  select="selectOrdersByUserId"                 fetchType="lazy"/> </resultMap> 

实现原理:通过代理对象延迟触发查询。常见实现为CGLIB/Javassist创建字节码代理或JDK动态代理包裹目标对象;aggressiveLazyLoading=false时仅在访问被标记为 lazy的属性时触发SQL,设置为 true则更“激进”,可能在更多方法调用中触发加载。

6. 高级特性

6.1 游标查询(Cursor)

/**  * 游标查询适合处理大量数据  */ try (SqlSession session = factory.openSession()) {     UserMapper mapper = session.getMapper(UserMapper.class);        // 返回游标,逐条读取     try (Cursor<User> cursor = mapper.selectAllUsers()) {         for (User user : cursor) {             processUser(user);         }     } } 

6.2 自定义ResultHandler

/**  * 自定义结果处理器实现流式处理  */ public class CustomResultHandler implements ResultHandler<User> {     private int count = 0;        @Override     public void handleResult(ResultContext<? extends User> context) {         User user = context.getResultObject();         processUser(user);         count++;              // 可以控制何时停止         if (count >= 1000) {             context.stop();         }     } }  // 使用 session.select("selectAllUsers", null, new CustomResultHandler()); 

6.3 多结果集处理

<select id="getUserAndOrders"          statementType="CALLABLE"          resultSets="users,orders">     {call get_user_and_orders(#{userId})} </select> 

7. 性能优化

7.1 避免N+1问题

// ❌ 错误:嵌套查询产生N+1问题 <collection property="orders" select="selectOrders"/>  // ✅ 正确:嵌套结果映射,一次JOIN查询 <collection property="orders" ofType="Order">     <id property="id" column="order_id"/> </collection> 

7.2 大数据量处理

// 1. 使用游标查询 Cursor<User> cursor = mapper.selectLargeData();  // 2. 使用ResultHandler session.select("selectLargeData", handler);  // 3. 分页查询 RowBounds bounds = new RowBounds(offset, limit); List<User> users = mapper.selectByPage(bounds); 

7.3 ResultMap缓存

// ResultMap配置会被缓存,重复使用无需重新解析 private List<UnMappedColumnAutoMapping> createAutomaticMappings(...) {     final String mapKey = resultMap.getId() + ":" + columnPrefix;        // 从缓存获取     List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);     if (autoMapping == null) {         // 创建并缓存         autoMapping = new ArrayList<>();         autoMappingsCache.put(mapKey, autoMapping);     }        return autoMapping; } 

8. 实践案例

8.1 完整映射示例

// 实体类 public class User {     private Long id;     private String name;     private Address address;      // 一对一     private List<Order> orders;   // 一对多 } 
<resultMap id="userDetailMap" type="User">     <id property="id" column="user_id"/>     <result property="name" column="user_name"/>        <association property="address" javaType="Address">         <id property="id" column="addr_id"/>         <result property="street" column="street"/>     </association>        <collection property="orders" ofType="Order">         <id property="id" column="order_id"/>         <result property="orderNo" column="order_no"/>     </collection> </resultMap> 

8.2 性能测试对比

public class ResultSetHandlerPerformanceTest {     private static SqlSessionFactory factory;      static {         try (InputStream is = Resources.getResourceAsStream("mybatis-config.xml")) {             factory = new SqlSessionFactoryBuilder().build(is);         } catch (Exception e) {             throw new RuntimeException(e);         }     }      public static void main(String[] args) {         try (SqlSession session = factory.openSession()) {             long time1 = testNestedQuery(session);             long time2 = testNestedResultMap(session);             System.out.printf("嵌套查询耗时: %dms%n", time1);             System.out.printf("嵌套结果耗时: %dms%n", time2);             System.out.printf("性能提升: %.1f%%%n", (time1 - time2) * 100.0 / time1);             // 实测通常 85%~95% 提升         }     }      private static long testNestedQuery(SqlSession session) {         long start = System.currentTimeMillis();         User user = session.selectOne("getUserWithNestedQuery", 1L);         user.getOrders().size(); // 触发 N+1         return System.currentTimeMillis() - start;     }      private static long testNestedResultMap(SqlSession session) {         long start = System.currentTimeMillis();         User user = session.selectOne("getUserWithNestedResultMap", 1L);         user.getOrders().size(); // 一次 JOIN         return System.currentTimeMillis() - start;     } } 

9. 常见问题

9.1 结果映射失败

问题:属性值为null

排查

  1. 检查列名是否匹配
  2. 检查ResultMap配置
  3. 检查TypeHandler
  4. 开启SQL日志

解决

<!-- 开启驼峰转换 --> <setting name="mapUnderscoreToCamelCase" value="true"/> 

9.2 N+1查询问题

解决方案

  1. 使用嵌套结果映射代替嵌套查询
  2. 开启延迟加载
  3. 使用批量查询优化

9.3 大数据量OOM

解决方案

// 使用游标或ResultHandler try (Cursor<User> cursor = mapper.selectAll()) {     for (User user : cursor) {         process(user);     } } 

9.4 源码调试指导

建议断点:

  • DefaultResultSetHandler.handleResultSets()
  • DefaultResultSetHandler.handleResultSet(...)
  • DefaultResultSetHandler.getRowValue(...)
  • DefaultResultSetHandler.applyAutomaticMappings(...)
  • DefaultResultSetHandler.applyPropertyMappings(...)
  • DefaultResultSetHandler.hasTypeHandlerForResultObject(...)

调试小贴士:

  • 开启日志:<setting name="logImpl" value="STDOUT_LOGGING"/>
  • 打印列到属性的映射关系,快速定位空值来源:
for (String column : rsw.getColumnNames()) {     System.out.println("column=" + column + ", value=" + rsw.getResultSet().getObject(column)); } 

10. 小结

核心职责

  • 将JDBC ResultSet映射为Java对象
  • 处理简单和复杂嵌套映射
  • 支持自动映射和手动配置
  • 实现延迟加载和游标查询

设计亮点

  • 灵活的ResultMap配置机制
  • 智能的自动映射策略
  • 高效的嵌套结果处理
  • 完善的类型转换体系

性能优化

  • 避免N+1查询问题
  • 合理使用嵌套结果映射
  • 大数据量使用游标或ResultHandler
  • 善用ResultMap缓存

思考题

  1. ResultSetHandler与ParameterHandler有什么本质区别?它们如何协作完成完整的数据流转?
  2. 嵌套查询和嵌套结果映射各有什么优缺点?在什么场景下应该选择哪种方式?
  3. 延迟加载的实现原理是什么?为什么需要使用代理对象?
  4. 如何设计一个通用的ResultSetHandler来支持多种扩展功能(如脱敏、审计、缓存等)?
  5. 在高并发场景下,ResultSetHandler的哪些设计可能成为性能瓶颈?如何优化?
发表评论

评论已关闭。

相关文章