0%

mybatis之ResultSetHandler结果集处理

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 {
// 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。

源码解析
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;

// 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 决定结果的存储方式(默认或自定义处理器)。

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) {
// 无自定义 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),分两种处理逻辑:

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()) {
// 1. 处理嵌套结果集(如关联对象:User 包含 Order 列表)
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 2. 处理简单结果集(如单表映射:User 直接映射自 user 表)
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();

// 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 列表),需要处理多行结果合并为一个对象(如一对多关联)。

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); // 逻辑分页跳过 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 对象,包括简单属性和嵌套关联属性。

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 {
// 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. 延迟加载配置

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 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)会拦截目标对象的方法调用,当检测到访问延迟加载属性时,触发加载:

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
// 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 方法执行查询:

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 {
// 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)支持流式查询,降低内存占用

欢迎关注我的其它发布渠道