第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 结果集处理流程图
sequenceDiagramparticipant 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 每个 ResultMapRSH->>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)endendRSH->>Stmt: getMoreResults()endRSH-->>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;// 获取第一个ResultSetResultSetWrapper rsw = getFirstResultSet(stmt);List<ResultMap> resultMaps = mappedStatement.getResultMaps();// 处理每个ResultSetwhile (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_noFROM t_user uLEFT JOIN t_order o ON u.id = o.user_idWHERE 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;@Overridepublic 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+1return System.currentTimeMillis() - start;}private static long testNestedResultMap(SqlSession session) {long start = System.currentTimeMillis();User user = session.selectOne("getUserWithNestedResultMap", 1L);user.getOrders().size(); // 一次 JOINreturn 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的哪些设计可能成为性能瓶颈?如何优化?