【源码解读之 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),命中则直接使用这些临时参数,保证动态生成的数据被正确绑定。

示例:

xml 复制代码
<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接口定义

java 复制代码
/**
 * 结果集处理器接口
 */
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 处理结果集主流程

java 复制代码
@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 处理每一行数据

java 复制代码
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读取列值

java 复制代码
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配置

xml 复制代码
<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 自动映射机制

xml 复制代码
<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问题)

xml 复制代码
<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)

xml 复制代码
<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 延迟加载

xml 复制代码
<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)

java 复制代码
/**
 * 游标查询适合处理大量数据
 */
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

java 复制代码
/**
 * 自定义结果处理器实现流式处理
 */
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 多结果集处理

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

7. 性能优化

7.1 避免N+1问题

java 复制代码
// ❌ 错误:嵌套查询产生N+1问题
<collection property="orders" select="selectOrders"/>

// ✅ 正确:嵌套结果映射,一次JOIN查询
<collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
</collection>

7.2 大数据量处理

java 复制代码
// 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缓存

java 复制代码
// 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 完整映射示例

java 复制代码
// 实体类
public class User {
    private Long id;
    private String name;
    private Address address;      // 一对一
    private List<Order> orders;   // 一对多
}
xml 复制代码
<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 性能测试对比

java 复制代码
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日志

解决

xml 复制代码
<!-- 开启驼峰转换 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

9.2 N+1查询问题

解决方案

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

9.3 大数据量OOM

解决方案

java 复制代码
// 使用游标或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"/>
  • 打印列到属性的映射关系,快速定位空值来源:
java 复制代码
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的哪些设计可能成为性能瓶颈?如何优化?
相关推荐
j***82702 小时前
Mybatis控制台打印SQL执行信息(执行方法、执行SQL、执行时间)
数据库·sql·mybatis
A***F1575 小时前
SpringBoot(整合MyBatis + MyBatis-Plus + MyBatisX插件使用)
spring boot·tomcat·mybatis
I***t7165 小时前
【MyBatis】spring整合mybatis教程(详细易懂)
java·spring·mybatis
代码or搬砖16 小时前
MyBatisPlus中的常用注解
数据库·oracle·mybatis
高级程序源19 小时前
springboot社区医疗中心预约挂号平台app-计算机毕业设计源码16750
java·vue.js·spring boot·mysql·spring·maven·mybatis
q***160821 小时前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
spring cloud·微服务·mybatis
忘记9261 天前
mybatis是什么
数据库·oracle·mybatis
q***92511 天前
Springboot3 Mybatis-plus 3.5.9
数据库·oracle·mybatis
k***45991 天前
【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis
xml·spring·mybatis