📌 PDF:大白话说Java面试题 — 05_Mybatis篇
第5题:MyBatis 是怎么解决实体类中的属性名和表中的字段名不一样的问题?
📚 回答:
- 核心考点: 字段名与属性名不一致是 ORM 框架的常见问题,但大厂面试不会只问"用 resultMap 或别名"。面试官期望你深入理解 MyBatis 的自动映射机制(
autoMappingBehavior的三个级别、mapUnderscoreToCamelCase的底层实现)、ResultMap 的解析与缓存(ResultMap对象在Configuration中的全局缓存)、嵌套映射(association/collection)的延迟加载原理,以及 MyBatis-Plus 的@TableField注解如何增强原生能力。面试官真正想判断的是:你是否能从配置、源码、框架扩展三个维度,给出体系化的映射方案。
1. 解决方案全景图
MyBatis 提供了 5 种核心方案 解决字段名与属性名不一致问题,按推荐优先级排序:
| 优先级 | 方案 | 适用场景 | 复杂度 | 维护性 |
|---|---|---|---|---|
| 1 | 全局驼峰映射 | 下划线命名 ↔ 驼峰命名(最常用) | 低 | 高 |
| 2 | resultMap 手动映射 | 复杂映射、嵌套对象、类型转换 | 中 | 高 |
| 3 | SQL 别名(AS) | 临时查询、简单差异 | 低 | 低 |
| 4 | @TableField 注解 | MyBatis-Plus 项目 | 低 | 高 |
| 5 | 自定义 TypeHandler | 特殊类型转换(如 JSON、枚举) | 高 | 中 |
2. 方案一:全局驼峰映射(最推荐)
- 2.1 配置方式 在
mybatis-config.xml或 Spring Boot 配置中开启:
<!-- mybatis-config.xml -->
<settings>
<!-- 开启驼峰命名自动映射:user_name → userName -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
# application.yml (Spring Boot)
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
- 2.2 底层原理
mapUnderscoreToCamelCase在DefaultResultSetHandler的自动映射阶段生效:
// DefaultResultSetHandler 中的自动映射逻辑
private boolean applyAutomaticMappings(...) {
List<UnMappedColumnAutoMapping> autoMappings = new ArrayList<>();
for (String columnName : columnNames) {
String propertyName = columnName;
if (configuration.isMapUnderscoreToCamelCase()) {
// 核心转换:USER_NAME → userName
propertyName = columnName.toLowerCase(Locale.ENGLISH)
.replace("_", "");
// 实际实现更复杂,处理首字母大写等
}
// 查找实体类中是否有该属性
if (metaObject.findProperty(propertyName, false) != null) {
autoMappings.add(new UnMappedColumnAutoMapping(...));
}
}
// 执行自动映射
for (UnMappedColumnAutoMapping mapping : autoMappings) {
final Object value = mapping.typeHandler.getResult(...);
metaObject.setValue(mapping.property, value);
}
}
转换规则:create_time → createTime,user_name → userName,order_item_id → orderItemId。
- 2.3 自动映射行为控制 MyBatis 通过
autoMappingBehavior控制自动映射的粒度:
| 级别 | 含义 | 适用场景 |
|---|---|---|
| NONE | 完全关闭自动映射 | 强制所有字段手动映射 |
| PARTIAL(默认) | 自动映射非嵌套 resultMap 的简单属性 | 单层映射,嵌套对象需手动 |
| FULL | 自动映射所有 resultMap,包括嵌套对象 | 简单项目,减少 XML 配置 |
<settings>
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
注意:FULL 模式在嵌套映射(association/collection)中可能产生性能问题,因为会自动映射所有嵌套属性。
3. 方案二:resultMap 手动映射(最灵活)
- 3.1 基本用法
resultMap是 MyBatis 最强大的映射工具,支持复杂类型转换、嵌套对象、集合映射:
<resultMap id="userResultMap" type="com.example.entity.User">
<!-- 主键映射 -->
<id property="userId" column="user_id" jdbcType="BIGINT"/>
<!-- 普通字段映射 -->
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<!-- 枚举类型映射(配合 TypeHandler) -->
<result property="status" column="status"
typeHandler="com.example.handler.StatusEnumTypeHandler"/>
</resultMap>
<select id="selectById" resultMap="userResultMap">
SELECT user_id, user_name, create_time, status FROM users WHERE user_id = #{id}
</select>
- 3.2 嵌套对象映射(association) 一对一关系映射:
<resultMap id="userWithDeptMap" type="com.example.entity.User">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<!-- 嵌套部门对象 -->
<association property="department" javaType="com.example.entity.Department">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
<select id="selectUserWithDept" resultMap="userWithDeptMap">
SELECT u.user_id, u.user_name, d.dept_id, d.dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.dept_id
WHERE u.user_id = #{id}
</select>
- 3.3 集合映射(collection) 一对多关系映射:
<resultMap id="userWithOrdersMap" type="com.example.entity.User">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<!-- 嵌套订单集合 -->
<collection property="orders" ofType="com.example.entity.Order">
<id property="orderId" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.user_id, u.user_name, o.order_id, o.order_no, o.amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id = #{id}
</select>
- 3.4 ResultMap 的缓存机制
ResultMap解析后会被缓存到Configuration中,避免重复解析:
public class Configuration {
// ResultMap 全局缓存:id → ResultMap
protected final Map<String, ResultMap> resultMaps = new StrictMap<>();
public void addResultMap(ResultMap rm) {
resultMaps.put(rm.getId(), rm);
// 检查局部缓存
checkLocallyForDiscriminatedNestedResultMaps(rm);
// 检查全局缓存
checkGloballyForDiscriminatedNestedResultMaps(rm);
}
}
4. 方案三:SQL 别名(快速但维护性差)
- 4.1 基本用法 在 SQL 中使用
AS为字段指定别名,使其与属性名一致:
<select id="selectUser" resultType="com.example.entity.User">
SELECT
user_id AS userId,
user_name AS userName,
create_time AS createTime,
is_deleted AS deleted
FROM users
WHERE user_id = #{id}
</select>
- 4.2 优缺点分析 | 优点 | 缺点 | | ---- | ---- | | 无需额外配置 | 每个 SQL 都要写别名,重复劳动 | | 直观可见 | 字段多时代码冗长 | | 适合临时查询 | 修改字段名需改所有 SQL | | 可针对特定 SQL 定制 | 无法处理嵌套映射 |
5. 方案四:MyBatis-Plus 的 @TableField(注解增强)
- 5.1 基本用法 MyBatis-Plus 通过
@TableField注解简化映射配置:
@Data
public class User {
@TableId(value = "user_id", type = IdType.AUTO)
private Long userId;
@TableField("user_name")
private String userName;
@TableField("create_time")
private LocalDateTime createTime;
@TableField(exist = false) // 非数据库字段
private List<Order> orders;
}
-
5.2 与 MyBatis 原生的对比 | 特性 | MyBatis 原生 | MyBatis-Plus | | ---- | ------------ | ------------ | | 配置方式 | XML resultMap / 全局配置 | 注解 + 自动推断 | | 下划线转驼峰 | 需手动开启
mapUnderscoreToCamelCase| 默认开启(可关闭) | | 非数据库字段 | 无直接支持 |@TableField(exist = false)| | 字段填充 | 无 |@TableField(fill = FieldFill.INSERT)| | 逻辑删除 | 需自行实现 |@TableLogic| | 乐观锁 | 需自行实现 |@Version| -
5.3 MyBatis-Plus 的自动映射增强 MP 在 MyBatis 基础上增加了自动推断映射:
// 即使不加 @TableField,MP 也会自动推断:
// user_name → userName(开启驼峰)
// create_time → createTime
// 但建议显式标注,避免歧义
6. 方案五:自定义 TypeHandler(特殊类型)
- 6.1 应用场景 当数据库字段类型与 Java 类型不匹配时(如 JSON 字符串 ↔ Java 对象、枚举 code ↔ 数据库整数):
// JSON 类型处理器
@MappedTypes(JsonObject.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<JsonObject> {
private static final Gson GSON = new Gson();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
JsonObject parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, GSON.toJson(parameter));
}
@Override
public JsonObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return json == null ? null : GSON.fromJson(json, JsonObject.class);
}
// ... 其他重载方法
}
<resultMap id="userResultMap" type="User">
<result property="extInfo" column="ext_info"
typeHandler="com.example.handler.JsonTypeHandler"/>
</resultMap>
7. 生产环境避坑指南
- 7.1 驼峰映射未生效的常见原因 开启
mapUnderscoreToCamelCase后仍映射失败:
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| 配置未加载 | 检查 mybatis-config.xml 路径 | 确认配置文件被正确加载 |
| 自定义 resultMap 覆盖 | 检查是否使用了 resultMap | resultMap 优先级高于自动映射 |
| 属性名不规范 | 检查 userID vs userId | 严格遵循驼峰命名 |
| 数据库字段含大写 | USER_NAME vs user_name | 统一数据库字段为小写下划线 |
| 嵌套对象未配置 | association/collection 中的字段 | 设置 autoMappingBehavior=FULL 或手动映射 |
- 7.2 resultMap 的继承与复用 避免重复定义,使用
extends继承:
<!-- 基础 resultMap -->
<resultMap id="BaseResultMap" type="User">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="createTime" column="create_time"/>
</resultMap>
<!-- 继承并扩展 -->
<resultMap id="UserWithDeptMap" type="User" extends="BaseResultMap">
<association property="department" javaType="Department">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
- 7.3 延迟加载与嵌套映射的性能陷阱 嵌套映射(association/collection)默认是立即加载,大数据量时性能差:
<!-- 开启延迟加载(懒加载) -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<resultMap id="userLazyMap" type="User">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<!-- fetchType="lazy" 延迟加载部门信息 -->
<association property="department" javaType="Department"
column="dept_id" select="selectDeptById" fetchType="lazy"/>
</resultMap>
- 7.4 避免 N+1 查询问题 嵌套映射的延迟加载可能导致 N+1 查询:
// ❌ N+1 问题:查询 100 个用户,触发 100 次部门查询
List<User> users = userMapper.selectAll();
for (User user : users) {
System.out.println(user.getDepartment().getDeptName()); // 每次触发懒加载
}
// ✅ 解决方案:使用 JOIN 一次性查询
<select id="selectAllWithDept" resultMap="userWithDeptMap">
SELECT u.*, d.dept_id, d.dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.dept_id
</select>
8. 面试官追问与高分回答模板
-
追问 1:“MyBatis 怎么解决字段名和属性名不一致?”
低分回答:“用 resultMap 或 SQL 别名。”(没有体系化,遗漏全局配置)
高分回答:
"MyBatis 提供了 5 种方案,按优先级排序:
- 全局驼峰映射:开启
mapUnderscoreToCamelCase=true,自动将user_name映射到userName,这是最推荐的方式,零配置且维护性高。 - resultMap 手动映射:通过
<result property="userName" column="user_name"/>精确控制,适合复杂映射、嵌套对象、类型转换。 - SQL 别名:
SELECT user_name AS userName,适合临时查询但维护性差。 - MyBatis-Plus 注解:
@TableField("user_name")简化配置,配合exist=false处理非数据库字段。 - 自定义 TypeHandler:处理 JSON、枚举等特殊类型转换。
生产环境中,优先开启全局驼峰映射,复杂场景用 resultMap,避免 SQL 别名导致的维护灾难。"
- 全局驼峰映射:开启
-
追问 2:“mapUnderscoreToCamelCase 的底层原理是什么?”
高分回答:
"
mapUnderscoreToCamelCase在DefaultResultSetHandler.applyAutomaticMappings()中生效:- 遍历结果集的列名(如
user_name) - 如果开启驼峰映射,将列名转换为驼峰格式(
user_name→userName) - 通过
MetaObject.findProperty()查找实体类中是否有该属性 - 找到匹配属性后,使用对应的
TypeHandler获取值并设置到对象中
注意:这个转换只在自动映射时生效。如果使用了
resultMap,resultMap的优先级高于自动映射,未在 resultMap 中配置的字段仍可能走自动映射(取决于autoMappingBehavior设置)。" - 遍历结果集的列名(如
-
追问 3:“resultMap 和 resultType 有什么区别?什么时候必须用 resultMap?”
高分回答:
"
resultType和resultMap的核心区别:维度 resultType resultMap 映射方式 自动映射(依赖驼峰或别名) 手动精确控制 嵌套对象 不支持 支持 association/collection 类型转换 默认 TypeHandler 可指定自定义 TypeHandler 复用性 无 可继承(extends) 性能 稍快(无解析开销) 稍慢(但缓存后无差异) 必须用 resultMap 的场景:
- 字段名与属性名完全无规律(如
usr_nm→userName) - 需要嵌套对象映射(一对一、一对多)
- 需要自定义 TypeHandler(如 JSON、枚举)
- 需要处理鉴别器(discriminator)动态映射
- 同一查询在不同场景需要不同映射"
- 字段名与属性名完全无规律(如
-
追问 4:“MyBatis-Plus 的 @TableField 和 MyBatis 的 resultMap 怎么选?”
高分回答:
"选择取决于项目技术栈和场景:
- 纯 MyBatis 项目:用
resultMap或全局驼峰配置,XML 集中管理映射关系。 - MyBatis-Plus 项目:优先用
@TableField注解,配合 MP 的自动推断,减少 XML 配置。 - 混合场景:简单映射用注解,复杂嵌套映射用 XML resultMap。
MP 的优势在于:
- 自动推断:即使不加
@TableField,开启驼峰后也能自动映射 - 非数据库字段:
@TableField(exist = false)直接标记 - 字段填充:
@TableField(fill = FieldFill.INSERT)自动填充创建时间 - 逻辑删除:
@TableLogic一行注解实现
但 MP 的注解方式在复杂嵌套映射(如多层级 association)时不如 XML resultMap 直观,此时建议混合使用。"
- 纯 MyBatis 项目:用
-
追问 5:“自动映射 behavior 的 NONE、PARTIAL、FULL 有什么区别?”
高分回答:
"
autoMappingBehavior控制 MyBatis 自动映射的粒度:- NONE:完全关闭自动映射。即使字段名和属性名完全一致,也不会自动映射。所有字段必须在
resultMap中显式配置。 - PARTIAL(默认):自动映射非嵌套的简单属性。对于
<resultMap>中未显式配置的属性,如果字段名匹配(或开启驼峰后匹配),自动映射。但<association>和<collection>中的嵌套属性不会自动映射。 - FULL:自动映射所有属性,包括嵌套对象中的属性。可以大幅减少 XML 配置,但可能带来性能问题(嵌套对象中不需要的字段也被映射),且可能映射到错误的属性。
生产环境推荐 PARTIAL(默认),简单属性自动映射减少配置,嵌套属性手动控制保证精确。"
- NONE:完全关闭自动映射。即使字段名和属性名完全一致,也不会自动映射。所有字段必须在
-
追问 6:“嵌套映射(association/collection)有什么性能陷阱?怎么优化?”
高分回答:
"嵌套映射有两个主要性能陷阱:
-
N+1 查询问题:
- 延迟加载(fetchType=‘lazy’)下,查询 100 个用户会触发 100 次部门查询。
- 优化:使用 JOIN 一次性查询,或关闭延迟加载用立即加载。
-
结果集膨胀问题:
- 一对多 JOIN 时,主表数据重复(如 1 个用户 3 个订单,用户数据出现 3 次)。
- 优化:使用嵌套 resultMap +
collection的column属性做子查询,或分页时在应用层组装。
-
内存占用问题:
- 嵌套对象多层级时,resultMap 解析和对象创建开销大。
- 优化:减少嵌套层级,使用扁平化 DTO,或开启
autoMappingBehavior=FULL减少 resultMap 配置。
最佳实践:简单关联用 JOIN + resultMap,复杂关联用延迟加载 + 缓存,大数据量用分页 + 扁平化 DTO。"
-
9. 方案选型速查表
| 场景 | 推荐方案 | 配置示例 | 注意事项 |
|---|---|---|---|
| 下划线 ↔ 驼峰 | 全局配置 | mapUnderscoreToCamelCase=true | 确保属性名严格驼峰 |
| 复杂字段映射 | resultMap | <result property="x" column="y"/> | 利用 extends 复用 |
| 嵌套对象(一对一) | resultMap + association | <association property="dept"/> | 考虑延迟加载 |
| 嵌套集合(一对多) | resultMap + collection | <collection property="orders"/> | 避免 N+1 查询 |
| 临时/简单查询 | SQL 别名 | SELECT col AS prop | 维护性差,不推荐 |
| MP 项目简单映射 | @TableField | @TableField("col") | 配合 exist=false |
| 非数据库字段 | @TableField(exist=false) | @TableField(exist = false) | MP 特有 |
| JSON/枚举转换 | 自定义 TypeHandler | 继承 BaseTypeHandler | 注册到 Configuration |
| 字段自动填充 | MP @TableField(fill) | @TableField(fill = INSERT) | 配合 MetaObjectHandler |
| 逻辑删除 | MP @TableLogic | @TableLogic | 自动处理删除查询 |
💡 面试官想要的满分总结:
MyBatis 解决字段名与属性名不一致的核心思路是分层映射:全局配置处理通用场景(驼峰映射),resultMap 处理复杂场景(嵌套、转换),注解简化配置(MP 项目),TypeHandler 处理特殊类型。
理解映射机制必须抓住三个关键点:
- 自动映射的优先级:
resultMap手动配置 >autoMappingBehavior自动映射 > 默认行为。mapUnderscoreToCamelCase只在自动映射时生效。- resultMap 的缓存设计:解析后的
ResultMap对象缓存到Configuration.resultMaps中,避免重复解析 XML,这是性能保障。- 嵌套映射的性能边界:association/collection 的延迟加载解决 N+1 问题,但引入新的查询开销;JOIN 查询避免 N+1,但导致结果集膨胀。没有银弹,需根据数据量选择。
工程实践上,优先开启全局驼峰映射(覆盖 80% 场景),复杂嵌套用 resultMap,MyBatis-Plus 项目用注解简化。永远避免 SQL 别名做长期方案——维护成本会随着字段增长呈指数级上升。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

2127

被折叠的 条评论
为什么被折叠?



