第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的哪些设计可能成为性能瓶颈?如何优化?
浙公网安备 33010602011771号