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/
面试考察点
-
映射机制理解:面试官不仅仅是想知道你 "会用"
resultMap,更是想考察你是否理解 MyBatis 是如何将数据库字段(下划线命名)映射到 Java 对象属性(驼峰命名)的,底层原理是什么。 -
配置灵活度:看你能否说出多种映射方式(自动映射、
resultMap、注解),以及各自的适用场景和优劣。 -
复杂映射能力:考察你是否掌握一对一、一对多等复杂关联映射的配置方式,这是实际开发中非常常见的需求。
核心答案
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 类型之间的转换(比如VARCHAR→String,BIGINT→Long)。
二、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_name → userName,created_time → createdTime。
底层原理:mapUnderscoreToCamelCase 为 true 时,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);
}
// ... 其他方法类似
}
面试高频追问
-
追问一:
resultType和resultMap有什么区别?resultType是自动映射,要求列名和属性名一致(或开启驼峰转换后一致);resultMap是手动映射,可以自由指定列名和属性名的对应关系,支持复杂关联映射。实际开发中,简单查询用resultType,复杂查询用resultMap。
-
追问二:
<id>标签有什么用?不写行不行?<id>标记主键字段,MyBatis 用它来判断两个结果对象是否是 "同一个" 实体。在嵌套结果映射中,<id>用于合并重复行。如果不写,可能导致一对多映射时出现重复数据。
-
追问三:MyBatis 的映射用了反射,性能会不会有问题?
- 会有一定开销,但 MyBatis 做了优化:解析阶段会将映射关系缓存为
ResultMapping对象,运行时直接使用缓存的元数据,避免重复反射。同时setter方法也会被缓存为Method对象,调用时不再重复查找。
- 会有一定开销,但 MyBatis 做了优化:解析阶段会将映射关系缓存为
常见面试变体
- 变体一:"MyBatis 的
resultMap和resultType有什么区别?" - 变体二:"MyBatis 如何处理数据库字段名和 Java 属性名不一致的问题?"
- 变体三:"MyBatis 的一对一和一对多映射怎么配置?"
记忆口诀
自动映射靠驼峰,手动映射用 resultMap;简单查询 resultType,复杂关联 association + collection;底层原理反射 + TypeHandler。
总结
MyBatis 字段映射的本质是通过 反射 + TypeHandler 将 ResultSet 的列值设置到 Java 对象属性上。简单映射用 resultType + 驼峰自动转换,复杂映射(一对一、一对多)用 resultMap 的 <association> 和 <collection> 标签。遇到特殊类型转换,可以自定义 TypeHandler 扩展映射能力。