第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外层构建装饰器,分阶段执行“验证 → 转换/加密 → 记录日志 → 委托设置参数”的流水线。 - 职责拆分:每个扩展功能独立实现(如
EncryptionParameterHandler、ValidationParameterHandler),通过组合或顺序调用复用。 - 与
TypeHandler协作:扩展仅在“取值”阶段介入,不破坏TypeHandler的类型转换职责,确保兼容性。 - 统一启用方式:可通过插件拦截或自定义
LanguageDriver控制何时创建自定义ParameterHandler。 - 失败处理:验证不通过抛出明确异常;加密失败回滚为原值或抛错,避免脏数据入库。
最小实现建议:在 setParameters(ps) 前先执行校验与转换,再委托给 DefaultParameterHandler 完成最终绑定。
1.1 本篇学习目标
- 深入理解ResultSetHandler的设计思想和核心职责
- 掌握DefaultResultSetHandler的结果映射流程
- 理解ResultMap配置和自动映射机制
- 掌握嵌套查询和嵌套结果映射
- 了解多结果集、游标查询等高级特性
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
排查:
- 检查列名是否匹配
- 检查ResultMap配置
- 检查TypeHandler
- 开启SQL日志
解决:
<!-- 开启驼峰转换 --> <setting name="mapUnderscoreToCamelCase" value="true"/>
9.2 N+1查询问题
解决方案:
- 使用嵌套结果映射代替嵌套查询
- 开启延迟加载
- 使用批量查询优化
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缓存
思考题
- ResultSetHandler与ParameterHandler有什么本质区别?它们如何协作完成完整的数据流转?
- 嵌套查询和嵌套结果映射各有什么优缺点?在什么场景下应该选择哪种方式?
- 延迟加载的实现原理是什么?为什么需要使用代理对象?
- 如何设计一个通用的ResultSetHandler来支持多种扩展功能(如脱敏、审计、缓存等)?
- 在高并发场景下,ResultSetHandler的哪些设计可能成为性能瓶颈?如何优化?