映射器原理
问题
MyBatis 的 Mapper 接口如何工作?resultMap 和 resultType 有什么区别?关联查询如何实现?
答案
Mapper 接口代理原理
Mapper 接口没有实现类,MyBatis 通过 JDK 动态代理生成代理对象:
方法全限定名 = 接口全限定名.方法名,对应 XML 中的 namespace.id。
resultType vs resultMap
<!-- 列名和属性名一致时使用 -->
<select id="getById" resultType="com.example.entity.User">
SELECT id, name, age, email FROM user WHERE id = #{id}
</select>
<!-- 列名和属性名不一致时,用 AS 别名解决 -->
<select id="getById" resultType="User">
SELECT id, user_name AS userName, create_time AS createTime FROM user WHERE id = #{id}
</select>
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="getById" resultMap="userMap">
SELECT id, user_name, create_time FROM user WHERE id = #{id}
</select>
| 对比 | resultType | resultMap |
|---|---|---|
| 适用场景 | 列名与属性名一致 | 列名与属性名不一致、复杂映射 |
| 关联查询 | 不支持 | 支持(association、collection) |
| 继承 | 不支持 | 支持(extends) |
| 使用方式 | 指定类的全限定名 | 引用已定义的 resultMap id |
大多数场景下开启驼峰自动映射即可,无需每次写 resultMap:
mybatis:
configuration:
map-underscore-to-camel-case: true # user_name → userName
关联查询(一对一、一对多)
<resultMap id="orderMap" type="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<!-- 嵌套结果映射(一条 SQL + JOIN) -->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
</resultMap>
<select id="getOrderWithUser" resultMap="orderMap">
SELECT o.id AS order_id, o.order_no, u.id AS user_id, u.name AS user_name
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
<resultMap id="userWithOrders" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 嵌套查询(两条 SQL,支持延迟加载) -->
<collection property="orders" ofType="Order"
select="com.example.mapper.OrderMapper.getByUserId"
column="id" fetchType="lazy"/>
</resultMap>
嵌套结果 vs 嵌套查询
| 方式 | 嵌套结果(JOIN) | 嵌套查询(子查询) |
|---|---|---|
| SQL 数量 | 1 条(JOIN) | N+1 条 |
| 性能 | 较好 | 有 N+1 问题 |
| 延迟加载 | 不支持 | 支持 |
| 适用场景 | 数据量小、必须一起使用 | 按需加载、数据独立 |
延迟加载(懒加载)
mybatis:
configuration:
lazy-loading-enabled: true # 全局开启延迟加载
aggressive-lazy-loading: false # 关闭积极加载
延迟加载基于动态代理:返回的关联对象是代理对象,当访问其属性时才触发实际的 SQL 查询。
常见面试问题
Q1: Mapper 接口的工作原理?
答案:
MyBatis 通过 JDK 动态代理 为 Mapper 接口生成代理对象(MapperProxy)。调用接口方法时,代理根据方法的全限定名(接口全名.方法名)找到对应的 MappedStatement,获取 SQL、参数映射和结果映射信息,委托给 SqlSession 执行。
Q2: resultMap 和 resultType 的区别?
答案:
resultType 直接指定返回类型,列名需与属性名一致(或开启驼峰映射)。resultMap 可以自定义列名到属性名的映射,支持关联查询(association/collection)、继承、鉴别器等复杂映射。简单查询用 resultType,复杂映射用 resultMap。
Q3: MyBatis 如何解决 N+1 查询问题?
答案:
N+1 问题来自嵌套查询(collection 的 select 方式):1 次主查询 + N 次子查询。解决方案:
- 使用嵌套结果映射(JOIN 一次查出所有数据)
- 开启延迟加载(只在需要时才查询)
- 使用BatchExecutor批量执行
Q4: #的底层实现原理?
答案:
#{} 在 MyBatis 解析阶段被替换为 ? 占位符,对应 JDBC 的 PreparedStatement。在执行阶段,ParameterHandler 通过 TypeHandler 将 Java 参数按正确的 JDBC 类型设置到 PreparedStatement 中(ps.setString(1, value))。预编译机制防止了 SQL 注入。
相关链接
- MyBatis 映射器文档
- 执行流程 - ResultSetHandler 如何映射结果
- 动态 SQL - SQL 标签