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

#Java#面试八股

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-caseuser_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 精确控制。

笔记加载中…