Mybatis 是怎么实现字段映射的?
结论先行:MyBatis 拿到 ResultSet 后,由 ResultSetHandler(默认 DefaultResultSetHandler)创建结果对象,再交给 ResultSetWrapper 与 MetaObject,按「列名 → 属性名」的规则反射赋值。映射方式有三种:resultType 的自动映射、resultMap 的显式映射、以及 TypeHandler 负责的类型转换。
1. 映射的完整链路
ResultSet
│ ResultSetWrapper 包装:读取列名、JdbcType、对应 TypeHandler
▼
DefaultResultSetHandler
│ ① 创建对象:ObjectFactory(默认反射无参构造)
│ ② 找映射:resultMap 显式配置 > 自动映射(列名≈属性名)
│ ③ 取类型处理器:UnknownTypeHandler 按 JdbcType 解析
▼
TypeHandler.getResult(rs, column)
│ ④ 值类型转换(DATE→LocalDate、TINYINT→Boolean …)
▼
MetaObject.setValue(对象, 属性路径, 值) ← 支持 user.address.city 这种嵌套路径
2. 自动映射规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 列名与属性名同名 | 直接赋值(不区分大小写) | id → id |
| 下划线转驼峰 | 需开启 map-underscore-to-camel-case | user_name → userName |
| 别名显式对齐 | SQL 里 AS 起别名 | u.name AS userName |
| 部分匹配 | autoMappingBehavior=PARTIAL(默认)只映射无嵌套的结果 | — |
| 强制全映射 | FULL 会尝试映射嵌套对象,风险高 | 一般不建议 |
| 关闭自动映射 | NONE,只能用 resultMap | 字段完全不规则时使用 |
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启后 user_name 自动映射 userName
auto-mapping-behavior: partial
3. resultMap:显式映射
当列名与属性名对不上、或需要处理一对一/一对多时,用 resultMap。
<resultMap id="OrderDetailMap" type="com.example.OrderDetail">
<id property="id" column="order_id"/>
<result property="amount" column="pay_amount" jdbcType="DECIMAL"/>
<result property="status" column="order_status"
typeHandler="com.example.OrderStatusTypeHandler"/>
<result property="createdAt" column="created_at"/>
<!-- 一对一 -->
<association property="user" javaType="com.example.User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
<!-- 一对多 -->
<collection property="items" ofType="com.example.OrderItem">
<id property="id" column="item_id"/>
<result property="skuId" column="sku_id"/>
<result property="count" column="item_count"/>
</collection>
</resultMap>
<select id="selectDetail" resultMap="OrderDetailMap">
SELECT o.id AS order_id, o.pay_amount, o.order_status, o.created_at,
u.id AS user_id, u.name AS user_name,
i.id AS item_id, i.sku_id, i.count AS item_count
FROM t_order o
LEFT JOIN t_user u ON u.id = o.user_id
LEFT JOIN t_order_item i ON i.order_id = o.id
WHERE o.id = #{id}
</select>
要点:<id> 不只是主键标记,它决定 MyBatis 如何判断「是不是同一个对象」——一对多映射时,若 id 配错(比如两个结果行的 id 相同),集合会被错误合并。
注解写法与 resultMap 等价:@Results/@Result 声明列与属性,@One/@Many 指向另一条语句的 id,规则完全一致。
4. 类型处理器(TypeHandler)
字段映射的另一半是「Java 类型 ↔ JDBC 类型」转换。自定义 TypeHandler 用于枚举、JSON 字段等。
@MappedTypes(OrderStatus.class)
@MappedJdbcTypes(JdbcType.INTEGER)
public class OrderStatusTypeHandler extends BaseTypeHandler<OrderStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
OrderStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}
@Override
public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
return OrderStatus.of(rs.getInt(columnName));
}
@Override
public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return OrderStatus.of(rs.getInt(columnIndex));
}
@Override
public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return OrderStatus.of(cs.getInt(columnIndex));
}
}
注册方式:mybatis.type-handlers-package=com.example.handler,或者在 resultMap 里显式指定 typeHandler。
5. 常见追问
追问一:开启驼峰映射后,user_name 和 userName 同时存在会怎样?
列名优先匹配精确的同名列,再尝试驼峰转换;两者都命中时可能覆盖赋值,属于应避免的列命名冲突。
追问二:为什么字段是 null 却没赋上值?
可能是 callSettersOnNulls=false(默认),null 值不会调用 setter,对象字段保持默认值。若业务区分「未查」与「为 null」,需把 call-setters-on-nulls 设为 true。
追问三:resultType 和 resultMap 能一起用吗?
不能同时生效,resultMap 优先级更高。resultType 走自动映射,resultMap 走显式映射,二选一。
追问四:构造函数注入怎么映射?
用 <constructor> 标签按参数顺序或 name 匹配,配合 @AutomapConstructor 可让 MyBatis 选择全参构造。字段不可变(final)时只能用这种方式。
追问五:自动映射能映射到嵌套对象吗?
默认 PARTIAL 不会。需要 FULL 或显式 association。更稳妥的是写 resultMap,避免列名冲突导致的错乱。
小结
字段映射可以拆成三步:建对象(ObjectFactory)→ 列到属性的对应(自动映射或 resultMap)→ 值的类型转换(TypeHandler)。日常开发 90% 的场景靠「下划线转驼峰 + 别名」就够了,剩下 10% 的复杂关联和自定义类型,用 resultMap 与自定义 TypeHandler 精确控制。