mybatis之ResultSetHandler结果集处理

MyBatis ResultSetHandler:结果集映射的核心机制(从映射流程到延迟加载)

在 MyBatis 执行查询操作时,StatementHandler 负责执行 SQL 语句,而 ResultSetHandler 则承担着将数据库返回的 ResultSet(结果集)转换为 Java 对象 的关键职责,同时还处理存储过程的输出参数。这一过程涉及结果集解析、对象映射、嵌套关联处理以及延迟加载等复杂逻辑。从 “接口定义→核心实现→映射流程→延迟加载” 四个维度,全面解析 ResultSetHandler 的工作原理。

ResultSetHandler 接口:结果处理的规范定义

ResultSetHandler 是 MyBatis 结果集处理的顶层接口,定义了三类核心操作,覆盖查询结果映射和存储过程输出参数处理:

public interface ResultSetHandler {
    // 1. 处理 SELECT 语句的结果集,映射为 Java 对象列表
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;

    // 2. 处理结果集,返回游标对象(用于流式查询)
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

    // 3. 处理存储过程的输出参数
    void handleOutputParameters(CallableStatement cs) throws SQLException;
}
  • handleResultSets:最核心的方法,将 Statement 执行后得到的 ResultSet 映射为 List 集合(可能包含一个或多个结果集);
  • handleCursorResultSets:用于大数据量查询的流式处理,返回 Cursor 对象支持逐条读取结果,避免内存溢出;
  • handleOutputParameters:针对存储过程,处理 OUTINOUT 类型的输出参数。

DefaultResultSetHandler:结果映射的唯一实现

MyBatis 仅提供 DefaultResultSetHandler 作为 ResultSetHandler 的实现类,它完整实现了结果集到 Java 对象的映射逻辑,包括简单对象映射嵌套关联映射延迟加载等核心功能。

1. 核心流程:handleResultSets 方法

handleResultSets 是结果集处理的入口方法,负责协调多个结果集的映射(如存储过程可能返回多个结果集),并将最终结果整理为 List。

源码解析
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    
    // 存储所有结果集映射后的对象列表
    List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;
    
    // 1. 获取第一个结果集(包装为 ResultSetWrapper,方便处理列信息)
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    
    // 2. 获取 MappedStatement 中配置的 resultMap(映射规则)
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    
    // 3. 校验结果集数量与 resultMap 数量匹配
    validateResultMapsCount(rsw, resultMapCount);
    
    // 4. 循环处理每个结果集(通常只有一个)
    while (rsw != null && resultMapCount > resultSetCount) {
        // 4.1 获取当前结果集对应的 ResultMap(映射规则)
        ResultMap resultMap = resultMaps.get(resultSetCount);
        
        // 4.2 根据 ResultMap 映射当前结果集,结果存入 multipleResults
        handleResultSet(rsw, resultMap, multipleResults, null);
        
        // 4.3 获取下一个结果集(如存在)
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet(); // 清理当前结果集的临时资源
        resultSetCount++;
    }
    
    // 5. 处理多结果集命名(针对存储过程的多结果集场景)
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
        while (rsw != null && resultSetCount < resultSets.length) {
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
                String nestedResultMapId = parentMapping.getNestedResultMapId();
                ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
                handleResultSet(rsw, resultMap, null, parentMapping);
            }
            rsw = getNextResultSet(stmt);
            cleanUpAfterHandlingResultSet();
            resultSetCount++;
        }
    }
    
    // 6. 如果只有一个结果集,直接返回其内部列表,否则返回 multipleResults
    return collapseSingleResultList(multipleResults);
}
关键步骤:
  • ResultSetWrapper:对 ResultSet 的包装,提供列名、列类型(JDBC 类型)等元信息,简化映射逻辑;
  • ResultMap:定义了 “数据库列→Java 属性” 的映射规则(如 column="user_id" property="userId"),是映射的核心配置;
  • 多结果集处理:支持存储过程返回多个结果集的场景,通过 resultMapsresultSets 配置分别映射。

2. 单个结果集映射:handleResultSet 方法

handleResultSet 负责处理单个 ResultSet,根据是否指定 ResultHandler 决定结果的存储方式(默认或自定义处理器)。

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, 
                            List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        if (parentMapping != null) {
            // 处理嵌套结果集(如关联对象的映射)
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else if (resultHandler == null) {
            // 无自定义 ResultHandler,使用默认处理器
            DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
            // 映射结果集行数据到对象,存入 defaultResultHandler
            handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
            multipleResults.add(defaultResultHandler.getResultList());
        } else {
            // 使用用户自定义的 ResultHandler 处理结果
            handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
    } finally {
        // 关闭结果集,释放资源
        closeResultSet(rsw.getResultSet());
    }
}
  • ResultHandler:允许用户自定义结果处理逻辑(如分页、过滤),默认使用 DefaultResultHandler 简单收集结果;
  • RowBounds:用于逻辑分页(offset 跳过行数,limit 最大行数),但需注意:逻辑分页会先查询所有数据再截断,大数据量下效率低,建议使用物理分页(如 LIMIT 语句)。

3. 行数据映射:handleRowValues 方法

handleRowValues 是结果集映射的核心执行者,根据 ResultMap 是否包含嵌套映射(如 associationcollection),分两种处理逻辑:

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, 
                           ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        // 1. 处理嵌套结果集(如关联对象:User 包含 Order 列表)
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        // 2. 处理简单结果集(如单表映射:User 直接映射自 user 表)
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}
(1)简单映射:handleRowValuesForSimpleResultMap

适用于单表映射(无关联对象),直接将 ResultSet 的每一行映射为一个 Java 对象。

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, 
                                              ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    
    // 1. 跳过 offset 行(逻辑分页)
    skipRows(resultSet, rowBounds);
    
    // 2. 逐行处理结果,直到达到 limit 或结果集结束
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        // 2.1 处理鉴别器(discriminator):根据字段值动态选择 ResultMap
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        
        // 2.2 映射当前行到 Java 对象
        Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
        
        // 2.3 将映射结果存入 ResultHandler
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}
关键子步骤:
  • skipRows:通过 resultSet.absolute(offset) 或循环 next() 跳过指定行数,实现逻辑分页;
  • resolveDiscriminatedResultMap:处理鉴别器(如 <discriminator column="type" javaType="int">),根据字段值动态选择不同的 ResultMap(如不同类型的用户映射到不同子类);
  • getRowValue:核心方法,将 ResultSet 当前行的列值映射到 Java 对象的属性(使用 TypeHandler 完成类型转换);
  • storeObject:将映射后的对象交给 ResultHandler 处理(默认收集到 List 中)。
(2)嵌套映射:handleRowValuesForNestedResultMap

适用于包含关联对象的映射(如 User 关联 Dept,或 Order 包含 OrderItem 列表),需要处理多行结果合并为一个对象(如一对多关联)。

private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, 
                                              ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds); // 逻辑分页跳过 offset 行
    
    Object rowValue = previousRowValue; // 缓存上一行的部分结果(用于合并关联数据)
    
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        // 1. 处理鉴别器,动态选择 ResultMap
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        
        // 2. 创建当前行的缓存键(用于识别同一对象的关联数据)
        CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
        
        // 3. 从缓存中获取已部分映射的对象(如一对多中已存在的父对象)
        Object partialObject = nestedResultObjects.get(rowKey);
        
        // 4. 处理有序结果集(如按父ID排序的关联数据)
        if (mappedStatement.isResultOrdered()) {
            if (partialObject == null && rowValue != null) {
                nestedResultObjects.clear();
                storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
            }
            // 合并当前行数据到对象(可能是部分映射的对象)
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        } else {
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
            if (partialObject == null) {
                // 首次映射完整对象,存入 ResultHandler
                storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
            }
        }
    }
    
    // 处理最后一行的剩余数据
    if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        previousRowValue = null;
    } else if (rowValue != null) {
        previousRowValue = rowValue; // 缓存用于下一次合并
    }
}
核心挑战与解决:
  • 关联数据合并:一对多关联中,父表数据会重复出现(如一个用户的多条订单),通过 rowKey 识别同一父对象,将多行订单合并到父对象的 orders 列表中;
  • 缓存机制nestedResultObjects 缓存部分映射的对象,避免重复创建,提高效率;
  • 有序结果集:通过 mappedStatement.isResultOrdered() 标识结果集是否按关联字段排序,优化合并逻辑。

4. 单行映射:getRowValue 方法

getRowValue 负责将 ResultSet单行数据映射为一个 Java 对象,包括简单属性和嵌套关联属性。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Object partialObject) throws SQLException {
    // 1. 创建或复用对象(partialObject 为嵌套映射中的部分对象)
    Object rowValue = partialObject;
    if (rowValue != null) {
        // 复用已有对象,仅更新属性
        applyPropertyMappings(rsw, resultMap, metaObjectForProperty(rowValue), columnPrefix);
    } else {
        // 2. 新建对象(通过 objectFactory 创建,支持构造函数注入)
        rowValue = createResultObject(rsw, resultMap, columnPrefix);
        if (rowValue != null && !hasTypeHandlerForResultObject(resultMap.getType())) {
            // 3. 为对象创建 MetaObject(MyBatis 的反射工具,方便属性操作)
            MetaObject metaObject = configuration.newMetaObject(rowValue);
            // 4. 处理自动映射(未在 resultMap 中显式配置的字段,按列名匹配属性)
            boolean foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
            // 5. 处理显式映射(resultMap 中配置的 <id>、<result>、<association> 等)
            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
            // 6. 若未映射任何属性,返回 null(避免创建空对象)
            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        }
    }
    return rowValue;
}
关键操作:
  • createResultObject:通过 ObjectFactory 创建目标对象,支持根据构造函数参数(constructorArg)注入初始值;
  • applyAutomaticMappings:自动映射未在 resultMap 中显式配置的字段(如数据库列 user_name 自动映射到 userName 属性);
  • applyPropertyMappings:处理显式映射,包括简单属性(result)、关联对象(association)、集合(collection)等,其中关联对象可能触发嵌套查询或延迟加载。

延迟加载:按需加载关联对象

在处理关联对象(如 User 关联 Dept)时,MyBatis 支持延迟加载(Lazy Loading):查询主对象时不立即加载关联对象,而是在首次访问关联对象的属性时才执行查询。这一机制通过动态代理实现,避免不必要的数据库查询,提升性能。

1. 延迟加载配置

延迟加载需通过全局配置或局部配置开启:

<!-- mybatis-config.xml 全局配置 -->
<settings>
    <!-- 开启延迟加载开关(默认 false) -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭积极加载(默认 false):仅在访问关联属性时加载,而非访问主对象时加载所有关联 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

<!-- Mapper.xml 局部配置(优先级高于全局) -->
<resultMap id="userResultMap" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- fetchType="lazy" 开启延迟加载;fetchType="eager" 立即加载 -->
    <association property="dept" column="dept_id" 
                 select="com.example.mapper.DeptMapper.getDeptById" 
                 fetchType="lazy"/>
</resultMap>

2. 实现原理:动态代理

由于关联对象通常是普通类(非接口),MyBatis 无法使用 JDK 动态代理,转而采用 CGLIBJavassist 生成目标类的子类(代理类),拦截属性访问方法(如 getDept()),触发延迟加载。

核心组件:
  • ProxyFactory:代理工厂接口,定义创建代理对象的方法,有两个实现类:
    • CglibProxyFactory:基于 CGLIB 生成代理(通过字节码技术生成子类);
    • JavassistProxyFactory:基于 Javassist 生成代理(轻量级字节码操作库)。
  • ResultLoader:保存延迟加载所需信息(如执行查询的 MappedStatement、参数、执行器等),负责实际执行查询。
  • ResultLoaderMap:管理一个对象中所有需要延迟加载的属性,记录每个属性对应的 ResultLoader

3. 代理对象的拦截逻辑

CglibProxyFactory 为例,代理类的拦截器(EnhancedResultObjectProxyImpl)会拦截目标对象的方法调用,当检测到访问延迟加载属性时,触发加载:

// CglibProxyFactory 内部类 EnhancedResultObjectProxyImpl 的 intercept 方法
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    String methodName = method.getName();
    synchronized (lazyLoader) {
        // 处理序列化相关方法(writeReplace)
        if ("writeReplace".equals(methodName)) {
            // ... 省略序列化逻辑
        }
        
        // 存在延迟加载属性,且当前方法不是 finalize(垃圾回收方法)
        if (lazyLoader.size() > 0 && !"finalize".equals(methodName)) {
            // 1. 非积极加载,且当前方法不是触发加载的方法(如 toString、hashCode 等)
            if (!aggressive && !lazyLoadTriggerMethods.contains(methodName)) {
                String property;
                if (PropertyNamer.isSetter(methodName)) {
                    // 2. 调用 setter 方法:移除该属性的延迟加载(已手动设置值)
                    property = PropertyNamer.methodToProperty(methodName);
                    lazyLoader.remove(property);
                } else if (PropertyNamer.isGetter(methodName)) {
                    // 3. 调用 getter 方法:检查是否为延迟加载属性
                    property = PropertyNamer.methodToProperty(methodName);
                    if (lazyLoader.hasLoader(property)) {
                        // 触发该属性的延迟加载
                        lazyLoader.load(property);
                    }
                }
            } else {
                // 4. 积极加载或触发方法:加载所有延迟属性
                lazyLoader.loadAll();
            }
        }
    }
    // 执行原方法(如 getDept(),此时已加载关联对象)
    return methodProxy.invokeSuper(enhanced, args);
}
拦截逻辑解析:
  • lazyLoaderResultLoaderMap 实例,记录所有需要延迟加载的属性(如 dept);
  • aggressive:对应 aggressiveLazyLoading 配置,false 表示按需加载(仅访问关联属性时加载);
  • lazyLoadTriggerMethods:触发加载的方法列表(默认包括 equalshashCodetoString 等,避免代理对象状态不一致);
  • load(property):调用 ResultLoader 执行查询,加载关联对象并赋值给属性。

4. 延迟加载的执行:ResultLoader

ResultLoader 封装了延迟加载的查询逻辑,当触发加载时,通过 loadResult 方法执行查询:

public class ResultLoader {
    // 执行延迟加载,返回结果对象
    public Object loadResult() throws SQLException {
        // 1. 执行查询,获取结果列表(如查询 dept_id=1 的部门)
        List<Object> list = selectList();
        // 2. 将列表转换为目标类型(如单个 Dept 对象)
        resultObject = resultExtractor.extractObjectFromList(list, targetType);
        return resultObject;
    }

    private <E> List<E> selectList() throws SQLException {
        Executor localExecutor = executor;
        // 检查线程和执行器状态,必要时创建新执行器
        if (Thread.currentThread().getId() != creatorThreadId || localExecutor.isClosed()) {
            localExecutor = newExecutor();
        }
        // 执行查询(复用 MyBatis 的查询逻辑)
        List<E> list = localExecutor.query(
            mappedStatement, parameterObject, 
            RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, 
            cacheKey, boundSql
        );
        // 清理临时执行器
        if (localExecutor != executor) {
            localExecutor.close(false);
        }
        return list;
    }
}
  • selectList:复用 MyBatis 的 Executor 执行查询,确保延迟加载的事务和缓存逻辑与主查询一致;
  • resultExtractor:将查询结果(List)转换为目标类型(如单个对象或集合)。

总结:ResultSetHandler 的核心价值

ResultSetHandler 是 MyBatis 结果映射的 “总导演”,其核心价值体现在:

  1. 结果集到对象的转换:通过 ResultMap 配置和 TypeHandler 类型转换,将 ResultSet 行数据映射为 Java 对象,支持简单属性和复杂关联;
  2. 灵活的映射策略:支持自动映射、显式映射、鉴别器动态映射,满足多样化的映射需求;
  3. 关联对象处理:通过嵌套映射解决一对一、一对多关联,避免手动拼接对象;
  4. 性能优化:通过延迟加载按需加载关联对象,减少不必要的数据库查询;通过游标(Cursor)支持流式查询,降低内存占用