今天深入解析MyBatis框架中非常核心的一个模块------结果映射模块。这个模块负责将数据库查询结果转换为Java对象,是MyBatis实现ORM功能的关键所在。
一、MyBatis整体架构与结果映射模块定位
在深入了解结果映射模块之前,我们先来看看MyBatis的整体架构。

从架构图可以看出,MyBatis采用了清晰的分层设计,而结果映射模块(ResultSetHandler)位于核心处理层,承担着至关重要的职责。
结果映射模块的五大核心职责
结果映射模块主要负责以下工作:
markdown
1. 处理查询结果集 - 将JDBC ResultSet转换为Java对象列表
2. 完成对象映射 - 根据ResultMap配置将数据库记录映射到Java对象
3. 处理复杂关联 - 支持一对一、一对多等嵌套查询
4. 实现延迟加载 - 按需加载关联对象,大幅提升查询性能
5. 创建对象实例 - 通过ObjectFactory实例化目标对象
ResultSetHandler接口定义
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;
}
二、ResultSetHandler架构设计
MyBatis为ResultSetHandler提供了灵活且强大的实现架构。

DefaultResultSetHandler核心实现
DefaultResultSetHandler是MyBatis提供的默认实现,包含了完整的结果处理逻辑:
java
public class DefaultResultSetHandler
implements ResultSetHandler {
private final Executor executor;
private final Configuration configuration;
private final MappedStatement mappedStatement;
private final TypeHandlerRegistry typeHandlerRegistry;
private final ObjectFactory objectFactory;
@Override
public List<Object> handleResultSets(Statement stmt)
throws SQLException {
List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 获取第一个ResultSet
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps =
mappedStatement.getResultMaps();
// 处理每个ResultSet
while (rsw != null &&
resultMapCount > resultSetCount) {
ResultMap resultMap =
resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap,
multipleResults, null);
rsw = getNextResultSet(stmt);
resultSetCount++;
}
return collapseSingleResultList(multipleResults);
}
}
ResultSetWrapper增强封装
ResultSetWrapper对原生ResultSet进行了封装,提供了更丰富的元数据信息:
arduino
public class ResultSetWrapper {
private final ResultSet resultSet;
private final List<String> columnNames;
private final List<JdbcType> jdbcTypes;
private final Map<String, Integer> columnIndexes;
// 获取指定位置的列名
public String getColumnName(int i) {
return columnNames.get(i);
}
// 获取指定位置的JDBC类型
public JdbcType getJdbcType(int i) {
return jdbcTypes.get(i);
}
// 获取合适的类型处理器
public TypeHandler<?> getTypeHandler(
Class<?> propertyType, String columnName) {
JdbcType jdbcType = getJdbcType(columnName);
return typeHandlerRegistry.getTypeHandler(
propertyType, jdbcType);
}
}
三、结果集映射完整流程
结果集映射是一个有序且复杂的过程,让我们逐步拆解。

步骤一:处理ResultSet
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 {
// 普通结果映射
DefaultResultHandler handler =
new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, handler,
rowBounds, null);
multipleResults.add(handler.getResultList());
}
} finally {
closeResultSet(rsw.getResultSet());
}
}
步骤二:处理行数据
根据是否包含嵌套映射,采用不同的处理策略:
scss
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);
}
}
步骤三:获取行值并创建对象
ini
private Object getRowValue(
ResultSetWrapper rsw, ResultMap resultMap)
throws SQLException {
final ResultLoaderMap lazyLoader =
new ResultLoaderMap();
// 创建目标对象
Object resultObject = createResultObject(
rsw, resultMap, lazyLoader);
if (resultObject != null &&
!hasTypeHandlerForResultObject(rsw, resultMap)) {
final MetaObject metaObject =
configuration.newMetaObject(resultObject);
boolean foundValues = false;
// 应用自动映射
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(
rsw, resultMap, metaObject, null);
}
// 处理属性映射
foundValues = applyPropertyMappings(
rsw, resultMap, metaObject,
lazyLoader, null) || foundValues;
return foundValues ? resultObject : null;
}
return resultObject;
}
自动映射机制
MyBatis支持自动映射,当数据库列名与Java属性名匹配时,会自动完成赋值:
ini
protected boolean applyAutomaticMappings(
ResultSetWrapper rsw, ResultMap resultMap,
MetaObject metaObject, String columnPrefix)
throws SQLException {
List<UnMappedColumnAutoMapping> autoMapping =
createAutomaticMappings(rsw, resultMap,
metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
for (UnMappedColumnAutoMapping mapping : autoMapping) {
final Object value = mapping.typeHandler
.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
metaObject.setValue(mapping.property, value);
foundValues = true;
}
}
}
return foundValues;
}
四、ResultMap配置详解
ResultMap是结果映射的核心配置,描述了如何将ResultSet映射到Java对象。

ResultMap核心属性
swift
public class ResultMap {
private final String id; // 唯一标识
private final Class<?> type; // 目标Java类型
private final List<ResultMapping> resultMappings;
// 映射列表
private final List<ResultMapping> idResultMappings;
// ID映射
private final Set<String> mappedColumns;
// 映射的列
private final Discriminator discriminator;
// 鉴别器
private final boolean hasNestedResultMaps;
// 是否有嵌套
private final Boolean autoMapping; // 自动映射开关
}
ResultMapping配置项
arduino
public class ResultMapping {
private String property; // Java属性名
private String column; // 数据库列名
private Class<?> javaType; // Java类型
private JdbcType jdbcType; // JDBC类型
private TypeHandler<?> typeHandler; // 类型处理器
private String nestedResultMapId; // 嵌套ResultMap
private String nestedQueryId; // 嵌套查询ID
private String columnPrefix; // 列前缀
private boolean lazy; // 延迟加载标识
}
三种典型配置示例
1. 基础ResultMap配置
ini
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_name" property="userName"
jdbcType="VARCHAR"/>
<result column="email" property="email"
jdbcType="VARCHAR"/>
<result column="create_time" property="createTime"
jdbcType="TIMESTAMP"/>
</resultMap>
2. 构造器映射配置
ini
<resultMap id="ConstructorResultMap" type="User">
<constructor>
<idArg column="id" javaType="long"/>
<arg column="user_name" javaType="String"/>
<arg column="email" javaType="String"/>
</constructor>
</resultMap>
3. 关联映射配置
xml
<resultMap id="UserWithOrderResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 一对一关联 -->
<association property="profile"
columnPrefix="profile_"
javaType="Profile"
resultMap="ProfileResultMap"/>
<!-- 一对多关联 -->
<collection property="orders" ofType="Order"
columnPrefix="order_"
resultMap="OrderResultMap"/>
</resultMap>
五、嵌套查询处理机制
MyBatis支持两种嵌套查询方式,各有优劣。

方式一:嵌套结果(Nested Results)
通过一次SQL查询获取所有数据,然后在内存中组装对象关系。
配置示例:
ini
<resultMap id="UserWithProfileResultMap" type="User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 嵌套结果映射 -->
<association property="profile" javaType="Profile">
<id column="profile_id" property="id"/>
<result column="phone" property="phone"/>
<result column="address" property="address"/>
</association>
</resultMap>
<select id="selectUserWithProfile"
resultMap="UserWithProfileResultMap">
SELECT
u.id as user_id,
u.user_name,
p.id as profile_id,
p.phone,
p.address
FROM t_user u
LEFT JOIN t_profile p ON u.id = p.user_id
WHERE u.id = #{id}
</select>
方式二:嵌套查询(Nested Queries)
通过多次SQL查询获取数据,支持延迟加载。
配置示例:
ini
<resultMap id="UserWithProfileResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 嵌套查询,支持延迟加载 -->
<association property="profile" column="id"
select="selectProfile"
fetchType="lazy"/>
</resultMap>
<select id="selectUserWithProfile"
resultMap="UserWithProfileResultMap">
SELECT id, user_name
FROM t_user
WHERE id = #{id}
</select>
<select id="selectProfile" resultType="Profile">
SELECT id, phone, address
FROM t_profile
WHERE user_id = #{id}
</select>
一对多关联处理
ini
<resultMap id="UserWithOrdersResultMap" type="User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 一对多集合映射 -->
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders"
resultMap="UserWithOrdersResultMap">
SELECT
u.id as user_id,
u.user_name,
o.id as order_id,
o.order_no,
o.amount
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
两种方式对比
| 维度 | 嵌套结果 | 嵌套查询 |
|---|---|---|
| SQL次数 | 1次 | N+1次 |
| 性能 | 较好 | 较差(可通过延迟加载优化) |
| 结果集大小 | 可能很大 | 较小 |
| SQL复杂度 | 复杂(多表JOIN) | 简单 |
| 适用场景 | 关联数据量小 | 关联数据量大或需延迟加载 |
六、延迟加载机制深度解析
延迟加载是MyBatis提升查询性能的重要手段,让我们深入了解其实现原理。

延迟加载原理
延迟加载基于代理模式实现,只有在真正访问关联对象时才触发查询。
代理对象创建:
scss
protected Object createResultObject(
ResultSetWrapper rsw, ResultMap resultMap,
ResultLoaderMap lazyLoader) throws SQLException {
final Class<?> resultType = resultMap.getType();
// 检查是否需要延迟加载
if (resultType.isInterface() ||
needsProxy(resultType)) {
// 创建代理对象
return proxyFactory.createProxy(
resultType, lazyLoader,
configuration.getObjectFactory());
}
return objectFactory.create(resultType);
}
全局延迟加载配置
xml
<settings>
<!-- 启用延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭侵入式延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
ResultLoader加载器
ResultLoader负责实际的延迟加载逻辑:
java
public class ResultLoader {
private final Configuration configuration;
private final Executor executor;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final Class<?> targetType;
// 执行延迟加载
public Object loadResult() throws SQLException {
List<Object> list = selectList();
resultObject = objectFactory.create(targetType);
if (resultObject != null) {
final MetaObject metaObject =
configuration.newMetaObject(resultObject);
if (resultObject instanceof Collection) {
((Collection) resultObject).addAll(list);
} else if (list.size() > 0) {
metaObject.setValue(list.get(0));
}
}
return resultObject;
}
private <E> List<E> selectList() throws SQLException {
return executor.query(
mappedStatement, parameterObject,
RowBounds.DEFAULT,
Executor.NO_RESULT_HANDLER);
}
}
代理工厂实现
typescript
public class JavassistProxyFactory
implements ProxyFactory {
@Override
public Object createProxy(
Object target, ResultLoaderMap lazyLoader,
ObjectFactory objectFactory) {
return EnhancedResultObjectProxyImpl
.createProxy(target, lazyLoader, objectFactory);
}
static class EnhancedResultObjectProxyImpl
implements MethodHandler {
@Override
public Object invoke(
Object enhanced, Method method,
Method[] proxiedMethods, Object[] args)
throws Throwable {
final String methodName = method.getName();
// 检查是否需要触发延迟加载
if (isLazyLoadTrigger(methodName)) {
lazyLoader.loadAll();
}
// 调用原方法
return method.invoke(enhanced, args);
}
}
}
延迟加载使用示例
ini
// Mapper接口定义
User selectUserWithLazyProfile(Long id);
// XML配置
<resultMap id="UserLazyResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 配置延迟加载 -->
<association property="profile" column="id"
select="selectProfile"
fetchType="lazy"/>
</resultMap>
<select id="selectUserWithLazyProfile"
resultMap="UserLazyResultMap">
SELECT id, user_name FROM t_user WHERE id = #{id}
</select>
// Java代码使用
User user = userMapper.selectUserWithLazyProfile(1L);
// 此时只执行了查询User的SQL,Profile未加载
String userName = user.getUserName();
// 访问User属性,不触发加载
Profile profile = user.getProfile();
// 访问Profile属性,触发延迟加载,执行selectProfile
七、鉴别器(Discriminator)使用
鉴别器类似于Java中的switch语句,可以根据某列的值动态选择使用哪个ResultMap。
应用场景
假设我们有一个车辆表,不同类型的车辆有不同的属性:
xml
<resultMap id="VehicleResultMap" type="Vehicle">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="type" property="type"/>
<!-- 鉴别器:根据type字段选择不同的ResultMap -->
<discriminator javaType="int" column="type">
<case value="1" resultMap="CarResultMap"/>
<case value="2" resultMap="TruckResultMap"/>
<case value="3" resultMap="BusResultMap"/>
</discriminator>
</resultMap>
<!-- 小汽车ResultMap -->
<resultMap id="CarResultMap" type="Car"
extends="VehicleResultMap">
<result column="seat_count" property="seatCount"/>
</resultMap>
<!-- 卡车ResultMap -->
<resultMap id="TruckResultMap" type="Truck"
extends="VehicleResultMap">
<result column="load_capacity"
property="loadCapacity"/>
</resultMap>
<!-- 公交车ResultMap -->
<resultMap id="BusResultMap" type="Bus"
extends="VehicleResultMap">
<result column="route_number"
property="routeNumber"/>
</resultMap>
八、最佳实践
8.1 ResultMap设计建议
1. 合理使用继承
通过extends属性避免重复配置,提高可维护性:
xml
<!-- 基础ResultMap -->
<resultMap id="BaseUserResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>
<!-- 继承基础ResultMap -->
<resultMap id="UserDetailResultMap" type="User"
extends="BaseUserResultMap">
<result column="email" property="email"/>
<result column="phone" property="phone"/>
</resultMap>
2. 显式配置映射
生产环境建议显式配置所有映射关系,避免依赖自动映射带来的不确定性:
xml
<resultMap id="UserResultMap" type="User"
autoMapping="false">
<!-- 显式配置所有字段映射 -->
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="email" property="email"/>
</resultMap>
3. 正确使用ID标签
使用标签标识主键,可以提升MyBatis的对象唯一性判断性能:
xml
<resultMap id="UserResultMap" type="User">
<!-- 使用id标签而不是result -->
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>
4. 使用列前缀避免冲突
多表关联查询时,使用columnPrefix避免列名冲突:
ini
<resultMap id="UserWithProfileResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 使用列前缀 -->
<association property="profile"
columnPrefix="p_"
resultMap="ProfileResultMap"/>
</resultMap>
<select id="selectUserWithProfile"
resultMap="UserWithProfileResultMap">
SELECT
u.id, u.user_name,
p.id as p_id,
p.phone as p_phone,
p.address as p_address
FROM t_user u
LEFT JOIN t_profile p ON u.id = p.user_id
</select>
8.2 嵌套查询选择策略
| 维度 | 嵌套结果 | 嵌套查询 |
|---|---|---|
| 查询次数 | 1次 | 可能N+1次 |
| 性能 | 好 | 较差(可用延迟加载优化) |
| 内存占用 | 结果集大时占用多 | 较少 |
| SQL复杂度 | 复杂(JOIN) | 简单 |
| 推荐场景 | 关联数据少,查询频繁 | 关联数据多,按需查询 |
8.3 性能优化
1. 合理使用延迟加载
避免N+1查询问题:
xml
<!-- 启用延迟加载 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 配置延迟加载 -->
<association property="profile" select="selectProfile"
fetchType="lazy"/>
2. 使用批量加载
开启延迟加载时,配置合理的批量加载策略:
xml
<settings>
<!-- 启用延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 延迟加载的触发方法 -->
<setting name="lazyLoadTriggerMethods"
value="equals,clone,hashCode,toString"/>
</settings>
3. ResultMap复用
通过extends减少重复配置,提高可维护性:
ini
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>
<resultMap id="DetailResultMap" type="User"
extends="BaseResultMap">
<result column="email" property="email"/>
</resultMap>
九、总结
MyBatis的结果映射模块是整个框架的核心组件之一,通过精心设计的ResultMap和ResultSetHandler,实现了从ResultSet到Java对象的灵活映射。