MyBatis结果映射模块详解

今天深入解析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对象的灵活映射。

相关推荐
酸菜牛肉汤面2 小时前
17、什么是脏读?幻读?不可重复读?
java·数据库·mysql
Coder_Boy_2 小时前
基于SpringAI的智能AIOps项目:微服务与DDD多模块融合设计概述
java·运维·人工智能·微服务·faiss
悟能不能悟2 小时前
如何处理java.time包类序列化问题,跨版本反序列化 Class对象可能抛出 InvalidClassException
java·开发语言
xxxxxxllllllshi2 小时前
深入解析单例模式:从原理到实战,掌握Java面试高频考点
java·开发语言·单例模式·面试
一直都在5722 小时前
Spring:Bean管理(二)
java·sql·spring
Miss_Chenzr2 小时前
Springboot快递信息管理52c05本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·数据库·spring boot
千寻技术帮2 小时前
基于SpringBoot的仿知乎知识问答系统
java·spring boot·毕业设计·论坛·文答
醉卧考场君莫笑2 小时前
数据分析理论基础
java·数据库·数据分析
廋到被风吹走2 小时前
【Java】【Jdk】Jdk11->Jdk17
java·开发语言·jvm