MyBatis 的 ResultSetHandler
StatementHandler
参数处理&结果处理,分别委托给ParameterHandler
&ResultSetHandler
(注意不是ResultHandler
)。上一篇文章介绍了ParameterHandler
,本文介绍ResultSetHandler
。
一、ResultSetHandler接口
我们先看下ResultSetHandler
接口定义:
java
public interface ResultSetHandler {
// 处理结果集,返回结果列表
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
// 处理游标结果集,返回Cursor对象(用于流式处理)
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
// 处理存储过程的输出参数
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
可以看到,ResultSetHandler
接口定义了三种不同场景下,处理JDBC结果集的方法。其核心职责是将数据库返回的ResultSet
转换为Java对象。
handleResultSets
:核心方法,处理JDBC的Statement
的结果集,注意普通查询和存储过程查询,都使用这个方法的结果集。handleCursorResultSets
:处理Mapper的返回值是Cursor
的方法。handleOutputParameters
:处理存储过程的输出参数,存储过程会使用handleResultSets
处理结果集,使用本方法,处理输出类型的参数。
二、DefaultResultSetHandler 的核心实现
MyBatis仅提供一个默认实现类DefaultResultSetHandler
,我们这里分析下核心的handleResultSets
方法源码。
1. 整体流程:handleResultSets 方法
handleResultSets
是处理查询结果集的核心方法,负责把Statement
的结果集转换为Java对象,需要注意,无论是普通的查询还是存储过程查询,都是调用这个方法。handleOutputParameters
只是处理存储过程的参数的(处理参数类型为OUT/INOUT类型的)。
注意这个方法名字,handleResultSets,有个s,暗示了结果集可能是多个
java
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 获取第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 获取MappedStatement中定义的结果集映射(可能有多个,对应多结果集)
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
// 第一部分:处理 MappedStatement 中定义的 resultMaps 对应的结果集
while (rsw != null && resultSetCount < resultMaps.size()) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理当前结果集,映射为Java对象
handleResultSet(rsw, resultMap, multipleResults, null);
// 获取下一个结果集(用于存储过程多结果集场景)
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
// 第二部分:处理MappedStatement中resultSets指定的附加结果集(用于解决N+1)
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
// 解决N+1,向父对象设置属性。
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, multipleResults, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 合并结果(如果只有一个结果集,直接返回该列表)
return collapseSingleResultList(multipleResults);
}
核心流程分为两部分
- 处理MappedStatement中定义的 resultMaps 对应的结果集。
- 处理MappedStatement中resultSets指定的附加结果集,用于解决N+1问题。
两部分都可能是多个!!!
原来,Mybatis可以支持一次获取多个结果集,很神奇。那么如何返回多个结果集呢?
- # Mybatis 如何返回多个结果集
- 也可以通过存储过程,这个官网有例子XML 映射器-结果映射(搜索ResultSets,可以解决N+1问题,这是第二个循环的来源)。
2. 单结果集处理:handleResultSet 方法
handleResultSet
负责处理单个结果集,根据是否有自定义 ResultHandler
选择不同的处理逻辑:
java
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 defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
// 有自定义ResultHandler时,使用其处理结果
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// 关闭结果集
closeResultSet(rsw.getResultSet());
}
}
3. 行记录映射:handleRowValues 与 getRowValue 方法
无论有无自定义ResultHandler
,都是调用handleRowValues
方法:
java
// 处理结果集中的行记录
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
// 处理嵌套结果映射(如包含association/collection)
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 处理普通结果映射(无嵌套)
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
看下处理普通映射的方法(处理嵌套的有点复杂,平时不怎么用,看起来头晕):
java
// 处理普通结果映射(逐行映射)
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
// 跳过RowBounds指定的偏移量,内存分页
skipRows(rsw.getResultSet(), rowBounds);
// 循环处理每行记录,直到达到RowBounds的限制或结果集结束
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
// 解析鉴别器(discriminator),动态选择ResultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 将当前行映射为Java对象
Object rowValue = getRowValue(rsw, discriminatedResultMap);
// 存储对象(调用ResultHandler处理)
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
Q:discriminator是什么鬼东西??
A:动态识别返回对象类型 ,比如我们定义了一个动物类,查询结果需要动态转为阿猫阿狗,就可以使用这个特性,可以在官网中查看使用方法:XML 映射器-结果映射(搜索discriminator)。
接下来看下核心的getRowValue
方法。
java
// 将单行记录映射为Java对象
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建结果对象(通过ObjectFactory)
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !typeHandlerRegistry.hasTypeHandler(rowValue.getClass())) {
// 获取结果对象的元信息(用于反射设置属性)
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 处理结果映射(ResultMapping),设置对象属性
if (shouldApplyAutomaticMappings(resultMap, false)) {
// 自动映射(未在ResultMap中显式配置的字段)
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
// 显式映射(ResultMap中配置的result/association/collection)
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
getRowValue
方法的核心逻辑:
- 创建结果对象 :通过
ObjectFactory
实例化目标对象(如User
)。 - 自动映射 :对
ResultMap
中未显式配置的字段,根据列名与属性名的匹配关系自动映射。 - 显式映射 :根据
ResultMap
中配置的result
、association
、collection
等标签,通过TypeHandler
将ResultSet
中的列值转换为对象属性。
4. applyAutomaticMappings
除了使用ResultMap标签进行映射,Mybatis还支持自动映射,也就是通过蛇形转驼峰的方式,将数据库字段,映射成Java属性:
java
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap,
MetaObject metaObject, String columnPrefix) throws SQLException {
// 1. 创建自动映射列表:筛选出未显式配置但存在匹配的字段
List<UnMappedColumnAutoMapping> autoMappings = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
if (autoMappings.isEmpty()) {
return false;
}
boolean foundValues = false;
// 2. 遍历自动映射列表,逐个处理字段
for (UnMappedColumnAutoMapping mapping : autoMappings) {
// 2.1 通过 TypeHandler 从结果集获取字段值(并转换为 Java 类型)
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
// 2.2 通过 MetaObject 给对象属性赋值(支持 null 值处理配置)
if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
// 给属性赋值(如 user.setName(value))
metaObject.setValue(mapping.property, value);
}
}
return foundValues;
}
关键步骤解析
- 创建自动映射列表(
createAutomaticMappings
) :获取未映射的字段,与JavaBean的属性建立映射关系,并根据JDBC的类型,绑定TypeHandler
,带有缓存,防止每次都重复映射。 - 通过
TypeHandler
转换类型 :熟悉的TypeHandler
,与上一节相反,这个是从JDBC转为Java类型。 - 通过
MetaObject
赋值 :利用 MyBatis 的反射工具MetaObject
给目标对象的属性赋值,支持配置callSettersOnNulls
(即使值为null
也调用 setter 方法),以及对基本类型(primitive
)的特殊处理(避免给基本类型赋null
导致异常)。
applyPropertyMappings
方法,为Java对象赋值的逻辑与这个相同,但还有些处理嵌套和延迟加载的逻辑,这里不深入研究了。
三、小结
ResultSetHandler
是负责将JDBC 结果集(ResultSet)转换为Java对象的核心组件。本文对他的实现做了浅显的分析,复杂的嵌套结果映射,以及嵌套结果的延迟加载,本文没有涉及(有点复杂)。
到此为止,MyBatis四大核心组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)已经分析完成,后续准备分析下Mybatis的插件机制,这个可能是我们了解Mybatis源码最重要的用途。