MyBatis ResultSetHandler:结果集映射的核心机制(从映射流程到延迟加载)
在 MyBatis 执行查询操作时,StatementHandler 负责执行 SQL 语句,而 ResultSetHandler 则承担着将数据库返回的 ResultSet(结果集)转换为 Java 对象 的关键职责,同时还处理存储过程的输出参数。这一过程涉及结果集解析、对象映射、嵌套关联处理以及延迟加载等复杂逻辑。从 “接口定义→核心实现→映射流程→延迟加载” 四个维度,全面解析 ResultSetHandler 的工作原理。
ResultSetHandler 接口:结果处理的规范定义
ResultSetHandler 是 MyBatis 结果集处理的顶层接口,定义了三类核心操作,覆盖查询结果映射和存储过程输出参数处理:
1 2 3 4 5 6 7 8 9 10
| 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; }
|
handleResultSets:最核心的方法,将 Statement 执行后得到的 ResultSet 映射为 List 集合(可能包含一个或多个结果集);
handleCursorResultSets:用于大数据量查询的流式处理,返回 Cursor 对象支持逐条读取结果,避免内存溢出;
handleOutputParameters:针对存储过程,处理 OUT 或 INOUT 类型的输出参数。
DefaultResultSetHandler:结果映射的唯一实现
MyBatis 仅提供 DefaultResultSetHandler 作为 ResultSetHandler 的实现类,它完整实现了结果集到 Java 对象的映射逻辑,包括简单对象映射、嵌套关联映射和延迟加载等核心功能。
1. 核心流程:handleResultSets 方法
handleResultSets 是结果集处理的入口方法,负责协调多个结果集的映射(如存储过程可能返回多个结果集),并将最终结果整理为 List。
源码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } 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++; } } return collapseSingleResultList(multipleResults); }
|
关键步骤:
ResultSetWrapper:对 ResultSet 的包装,提供列名、列类型(JDBC 类型)等元信息,简化映射逻辑;
ResultMap:定义了 “数据库列→Java 属性” 的映射规则(如 column="user_id" property="userId"),是映射的核心配置;
- 多结果集处理:支持存储过程返回多个结果集的场景,通过
resultMaps 或 resultSets 配置分别映射。
2. 单个结果集映射:handleResultSet 方法
handleResultSet 负责处理单个 ResultSet,根据是否指定 ResultHandler 决定结果的存储方式(默认或自定义处理器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); multipleResults.add(defaultResultHandler.getResultList()); } else { handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } finally { closeResultSet(rsw.getResultSet()); } }
|
ResultHandler:允许用户自定义结果处理逻辑(如分页、过滤),默认使用 DefaultResultHandler 简单收集结果;
RowBounds:用于逻辑分页(offset 跳过行数,limit 最大行数),但需注意:逻辑分页会先查询所有数据再截断,大数据量下效率低,建议使用物理分页(如 LIMIT 语句)。
3. 行数据映射:handleRowValues 方法
handleRowValues 是结果集映射的核心执行者,根据 ResultMap 是否包含嵌套映射(如 association、collection),分两种处理逻辑:
1 2 3 4 5 6 7 8 9 10
| public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } else { handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } }
|
(1)简单映射:handleRowValuesForSimpleResultMap
适用于单表映射(无关联对象),直接将 ResultSet 的每一行映射为一个 Java 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap, null); 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 列表),需要处理多行结果合并为一个对象(如一对多关联)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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); Object rowValue = previousRowValue; while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); Object partialObject = nestedResultObjects.get(rowKey); 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) { 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 对象,包括简单属性和嵌套关联属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Object partialObject) throws SQLException { Object rowValue = partialObject; if (rowValue != null) { applyPropertyMappings(rsw, resultMap, metaObjectForProperty(rowValue), columnPrefix); } else { rowValue = createResultObject(rsw, resultMap, columnPrefix); if (rowValue != null && !hasTypeHandlerForResultObject(resultMap.getType())) { MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); foundValues = applyPropertyMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; 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. 延迟加载配置
延迟加载需通过全局配置或局部配置开启:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
<resultMap id="userResultMap" type="User"> <id column="id" property="id"/> <result column="name" property="name"/> <association property="dept" column="dept_id" select="com.example.mapper.DeptMapper.getDeptById" fetchType="lazy"/> </resultMap>
|
2. 实现原理:动态代理
由于关联对象通常是普通类(非接口),MyBatis 无法使用 JDK 动态代理,转而采用 CGLIB 或 Javassist 生成目标类的子类(代理类),拦截属性访问方法(如 getDept()),触发延迟加载。
核心组件:
ProxyFactory:代理工厂接口,定义创建代理对象的方法,有两个实现类:
CglibProxyFactory:基于 CGLIB 生成代理(通过字节码技术生成子类);
JavassistProxyFactory:基于 Javassist 生成代理(轻量级字节码操作库)。
ResultLoader:保存延迟加载所需信息(如执行查询的 MappedStatement、参数、执行器等),负责实际执行查询。
ResultLoaderMap:管理一个对象中所有需要延迟加载的属性,记录每个属性对应的 ResultLoader。
3. 代理对象的拦截逻辑
以 CglibProxyFactory 为例,代理类的拦截器(EnhancedResultObjectProxyImpl)会拦截目标对象的方法调用,当检测到访问延迟加载属性时,触发加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { String methodName = method.getName(); synchronized (lazyLoader) { if ("writeReplace".equals(methodName)) { } if (lazyLoader.size() > 0 && !"finalize".equals(methodName)) { if (!aggressive && !lazyLoadTriggerMethods.contains(methodName)) { String property; if (PropertyNamer.isSetter(methodName)) { property = PropertyNamer.methodToProperty(methodName); lazyLoader.remove(property); } else if (PropertyNamer.isGetter(methodName)) { property = PropertyNamer.methodToProperty(methodName); if (lazyLoader.hasLoader(property)) { lazyLoader.load(property); } } } else { lazyLoader.loadAll(); } } } return methodProxy.invokeSuper(enhanced, args); }
|
拦截逻辑解析:
lazyLoader:ResultLoaderMap 实例,记录所有需要延迟加载的属性(如 dept);
aggressive:对应 aggressiveLazyLoading 配置,false 表示按需加载(仅访问关联属性时加载);
lazyLoadTriggerMethods:触发加载的方法列表(默认包括 equals、hashCode、toString 等,避免代理对象状态不一致);
load(property):调用 ResultLoader 执行查询,加载关联对象并赋值给属性。
4. 延迟加载的执行:ResultLoader
ResultLoader 封装了延迟加载的查询逻辑,当触发加载时,通过 loadResult 方法执行查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class ResultLoader { public Object loadResult() throws SQLException { List<Object> list = selectList(); 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(); } 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 结果映射的 “总导演”,其核心价值体现在:
- 结果集到对象的转换:通过
ResultMap 配置和 TypeHandler 类型转换,将 ResultSet 行数据映射为 Java 对象,支持简单属性和复杂关联;
- 灵活的映射策略:支持自动映射、显式映射、鉴别器动态映射,满足多样化的映射需求;
- 关联对象处理:通过嵌套映射解决一对一、一对多关联,避免手动拼接对象;
- 性能优化:通过延迟加载按需加载关联对象,减少不必要的数据库查询;通过游标(
Cursor)支持流式查询,降低内存占用