Mybatis 是怎么实现字段映射的?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 映射机制理解:面试官不仅仅是想知道你 "会用" resultMap,更是想考察你是否理解 MyBatis 是如何将数据库字段(下划线命名)映射到 Java 对象属性(驼峰命名)的,底层原理是什么。

  2. 配置灵活度:看你能否说出多种映射方式(自动映射、resultMap、注解),以及各自的适用场景和优劣。

  3. 复杂映射能力:考察你是否掌握一对一、一对多等复杂关联映射的配置方式,这是实际开发中非常常见的需求。

核心答案

MyBatis 的字段映射本质上是将 JDBC ResultSet 中的列 转换为 Java 对象的属性,主要提供了 4 种映射方式

映射方式适用场景灵活度推荐度
自动映射(驼峰转换)字段名和属性名符合驼峰规则简单查询推荐
resultType + 别名字段名和属性名不一致但不多快速开发
resultMap 手动映射复杂映射、一对一、一对多生产推荐
注解 @Results不想写 XML偏好注解的团队

一句话结论:MyBatis 通过 ResultSet 元数据获取列名,再通过反射将值设置到 Java 对象的对应属性上。简单查询用自动映射,复杂映射用 resultMap

深度解析

一、字段映射的整体流程

MyBatis 字段映射的核心流程可以拆解为以下几个关键步骤:

  • 步骤一(SQL 执行):MyBatis 解析 Mapper XML 中的 SQL,通过 JDBC 执行查询,获得 ResultSet 结果集。
  • 步骤二(遍历结果集):逐行遍历 ResultSet,每一行都需要映射为一个 Java 对象。
  • 步骤三(映射策略选择):根据配置决定使用 resultType 自动映射还是 resultMap 手动映射。
  • 步骤四(核心映射操作):这是最关键的一步,包含 4 个子操作——获取列名、匹配属性、类型转换、反射赋值。其中 TypeHandler 负责处理数据库类型和 Java 类型之间的转换(比如 VARCHARStringBIGINTLong)。

二、4 种映射方式详解

方式一:自动映射(驼峰转换)

数据库字段通常用下划线命名(user_name),Java 属性用驼峰命名(userName)。MyBatis 提供了自动驼峰转换功能:

<!-- mybatis-config.xml 开启驼峰转换(默认开启) -->
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- Mapper XML:直接用 resultType 指定 Java 类型 -->
<select id="selectById" resultType="com.example.entity.User">
    SELECT id, user_name, email, created_time
    FROM t_user
    WHERE id = #{id}
</select>

MyBatis 自动完成映射:user_nameuserNamecreated_timecreatedTime

底层原理mapUnderscoreToCamelCasetrue 时,MyBatis 在自动映射阶段会将列名中的下划线去掉,并将下划线后第一个字母大写,然后与 Java 属性名匹配。

方式二:SQL 别名

当字段名和属性名差异较大,不想写 resultMap 时,可以在 SQL 中用别名:

<select id="selectById" resultType="com.example.entity.User">
    SELECT
        id,
        user_name AS userName,
        email_addr AS emailAddr,
        created_time AS createdTime
    FROM t_user
    WHERE id = #{id}
</select>

这种方式简单直接,但 SQL 中别名太多会导致 SQL 可读性下降。

方式三:resultMap 手动映射(重点)

这是最灵活、生产环境最常用的方式,支持复杂映射:

<!-- 定义 resultMap -->
<resultMap id="userResultMap" type="com.example.entity.User">
    <!-- 主键映射:id 标签可以触发缓存优化 -->
    <id column="id" property="id"/>

    <!-- 普通字段映射 -->
    <result column="user_name" property="userName"/>
    <result column="email_addr" property="emailAddr"/>
    <result column="created_time" property="createdTime"/>
</resultMap>

<!-- 使用 resultMap -->
<select id="selectById" resultMap="userResultMap">
    SELECT id, user_name, email_addr, created_time
    FROM t_user
    WHERE id = #{id}
</select>

<id><result> 的区别

标签作用说明
<id>映射主键字段标记主键列,MyBatis 用它来判断两个对象是否 "相同"(影响缓存和嵌套结果的合并)
<result>映射普通字段普通的列到属性映射

方式四:复杂关联映射

resultMap 的真正威力在于处理一对一一对多关联关系:

一对一(<association>

// 用户有一个部门信息
public class User {
    private Long id;
    private String userName;
    private Department dept;  // 一对一关联
}
<resultMap id="userWithDeptMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="user_name" property="userName"/>

    <!-- 一对一:association -->
    <association property="dept" javaType="com.example.entity.Department">
        <id column="dept_id" property="id"/>
        <result column="dept_name" property="deptName"/>
    </association>
</resultMap>

<select id="selectUserWithDept" resultMap="userWithDeptMap">
    SELECT u.id, u.user_name, d.id AS dept_id, d.dept_name
    FROM t_user u
    LEFT JOIN t_department d ON u.dept_id = d.id
    WHERE u.id = #{id}
</select>

一对多(<collection>

// 用户有多篇文章
public class User {
    private Long id;
    private String userName;
    private List<Article> articles;  // 一对多关联
}
<resultMap id="userWithArticlesMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="user_name" property="userName"/>

    <!-- 一对多:collection -->
    <collection property="articles" ofType="com.example.entity.Article">
        <id column="article_id" property="id"/>
        <result column="title" property="title"/>
        <result column="content" property="content"/>
    </collection>
</resultMap>

<select id="selectUserWithArticles" resultMap="userWithArticlesMap">
    SELECT u.id, u.user_name,
           a.id AS article_id, a.title, a.content
    FROM t_user u
    LEFT JOIN t_article a ON u.id = a.user_id
    WHERE u.id = #{id}
</select>

三、底层原理:反射 + TypeHandler

MyBatis 字段映射的核心就是反射类型处理器(TypeHandler

单个字段的映射分三步完成:

  • 获取列值:通过 ResultSet.getString() / getInt() 等方法,根据 JDBC 类型获取数据库列值。
  • TypeHandler 类型转换:MyBatis 内置了大量 TypeHandler,负责 JDBC 类型和 Java 类型之间的转换。比如 BigDecimalTypeHandler 将数据库的 DECIMAL 转为 Java 的 BigDecimal。也可以自定义 TypeHandler 处理特殊类型(比如 JSON 字段转 Java 对象)。
  • 反射赋值:通过反射获取 Java 对象的 setter 方法,调用 setter.invoke() 完成赋值。

四、自定义 TypeHandler 示例

当数据库字段类型和 Java 类型无法直接对应时(比如 JSON 字段),可以自定义 TypeHandler

// 将数据库中的 JSON 字符串自动转为 Java 的 List
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
            List<String> parameter, JdbcType jdbcType) throws SQLException {
        // Java → 数据库:List 转 JSON 字符串
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        // 数据库 → Java:JSON 字符串转 List
        String json = rs.getString(columnName);
        return JSON.parseArray(json, String.class);
    }
    // ... 其他方法类似
}

面试高频追问

  1. 追问一resultTyperesultMap 有什么区别?

    • resultType 是自动映射,要求列名和属性名一致(或开启驼峰转换后一致);resultMap 是手动映射,可以自由指定列名和属性名的对应关系,支持复杂关联映射。实际开发中,简单查询用 resultType,复杂查询用 resultMap
  2. 追问二<id> 标签有什么用?不写行不行?

    • <id> 标记主键字段,MyBatis 用它来判断两个结果对象是否是 "同一个" 实体。在嵌套结果映射中,<id> 用于合并重复行。如果不写,可能导致一对多映射时出现重复数据。
  3. 追问三:MyBatis 的映射用了反射,性能会不会有问题?

    • 会有一定开销,但 MyBatis 做了优化:解析阶段会将映射关系缓存为 ResultMapping 对象,运行时直接使用缓存的元数据,避免重复反射。同时 setter 方法也会被缓存为 Method 对象,调用时不再重复查找。

常见面试变体

  • 变体一:"MyBatis 的 resultMapresultType 有什么区别?"
  • 变体二:"MyBatis 如何处理数据库字段名和 Java 属性名不一致的问题?"
  • 变体三:"MyBatis 的一对一和一对多映射怎么配置?"

记忆口诀

自动映射靠驼峰,手动映射用 resultMap;简单查询 resultType,复杂关联 association + collection;底层原理反射 + TypeHandler。

总结

MyBatis 字段映射的本质是通过 反射 + TypeHandlerResultSet 的列值设置到 Java 对象属性上。简单映射用 resultType + 驼峰自动转换,复杂映射(一对一、一对多)用 resultMap<association><collection> 标签。遇到特殊类型转换,可以自定义 TypeHandler 扩展映射能力。