前言
刚接触 MyBatis 时,我一直觉得 Mapper 接口有点“神奇”:接口明明没有实现类,注入后却能直接调用;XML 里写一段 SQL,返回值又能自动变成 Java 对象。
后面再回头看,会发现 MyBatis 并没有凭空做什么。它的核心逻辑很清楚:启动时解析 SQL 和映射规则,运行时为 Mapper 接口创建代理对象,再由代理对象找到对应 SQL 并交给执行器处理。
本文从实际开发中经常被问到的五个问题入手,把 MyBatis 与 JPA、缓存、结果映射和 Mapper 代理串起来讲清楚。
一、MyBatis 和 JPA 有什么区别
先纠正一个常见说法:MyBatis 与 JPA 并不是完全对等的两个框架。
MyBatis 是一个 SQL 映射框架,开发者自己编写 SQL,它负责参数绑定、SQL 执行和结果映射。JPA 则是 Java 持久化规范,Hibernate 是常见实现,Spring Data JPA 又在 JPA 之上封装了 Repository 等能力。
如果系统中复杂查询多、SQL 调优要求高,MyBatis 往往更顺手;如果业务以标准 CRUD 和领域对象关系为主,JPA 可以减少不少重复代码。
二、MyBatis 二级缓存是怎样工作的
谈二级缓存前,需要先区分一级缓存。
-
一级缓存属于 SqlSession,默认开启。同一个 SqlSession 中执行相同查询,可以直接复用结果。
-
二级缓存属于 Mapper 的 namespace,可以在多个 SqlSession 之间共享。
在 Mapper XML 中加入下面的配置即可开启当前命名空间的二级缓存:
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
</mapper>
一次查询可以简单理解为:先检查当前会话的一级缓存,再根据缓存执行器检查二级缓存,未命中时才访问数据库。查询结果不会立刻进入二级缓存,通常要等 SqlSession 提交或关闭后才会写入。
假设两个 SqlSession 调用同一个 UserMapper 查询用户:第一个会话查询数据库并提交后,第二个会话再执行相同语句,就可能命中二级缓存。
当同一 namespace 下执行 insert、update 或 delete 时,MyBatis 默认会清空相关缓存,避免继续读取旧数据。查询标签还可以通过属性控制行为:
<select id="findById"
resultType="com.example.domain.User"
useCache="true"
flushCache="false">
select id, user_name from user where id = #{id}
</select>
二级缓存看起来方便,但实际项目中要谨慎使用。它以 namespace 为边界,如果一张表被多个 Mapper 修改,缓存未必能被正确感知和清理;分布式部署下,各实例的本地缓存也不一致。因此,对一致性要求高的业务,团队往往更愿意使用 Redis 等边界更明确的缓存方案。
三、resultType 和 resultMap 的区别
二者都用于告诉 MyBatis“查询结果应该怎样转换成 Java 对象”,区别在于映射能力。
resultType:适合简单映射
当列名与 Java 属性名能够直接对应时,使用 resultType 最省事:
<select id="findById" resultType="com.example.domain.User">
select id, user_name as userName
from user
where id = #{id}
</select>
这里通过 SQL 别名让 user_name 对应 userName。如果开启下划线转驼峰配置,很多场景甚至不需要别名。
resultMap:适合复杂映射
字段名差异较大,或者存在嵌套对象、一对一、一对多关系时,resultMap 更合适:
<resultMap id="userMap" type="com.example.domain.User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<collection property="roles"
ofType="com.example.domain.Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
</collection>
</resultMap>
<select id="findWithRoles" resultMap="userMap">
select …
</select>
可以把 resultType 理解为“告诉 MyBatis 返回什么类型”,把 resultMap 理解为“明确描述每个字段怎么装配”。一条 SQL 通常二选一,不能同时指定两者。
四、Mapper 接口有哪些绑定方式
常见绑定方式主要有 XML 和注解两种。
1. XML 绑定
XML 的 namespace 必须是 Mapper 接口的全限定名,SQL 标签的 id 必须与接口方法名对应:
public interface UserMapper {
User findById(Long id);
}
<mapper namespace="com.example.mapper.UserMapper">
<select id="findById" resultType="com.example.domain.User">
select * from user where id = #{id}
</select>
</mapper>
MyBatis 最终使用的语句标识是:
com.example.mapper.UserMapper.findById
2. 注解绑定
简单 SQL 也可以直接写在接口方法上:
@Mapper
public interface UserMapper {
@Select("select * from user where id = #{id}")
User findById(Long id);
}
@Mapper 或 @MapperScan 的作用是扫描接口、注册 Mapper Bean;真正将方法与 SQL 关联起来的,是 XML 中的 namespace + id 或方法上的 SQL 注解。
复杂 SQL 更适合 XML,简单且稳定的 SQL 使用注解会更紧凑。项目中可以共存,但最好建立统一规范,避免同一条语句在两处维护。
五、Mapper 接口为什么不需要实现类
Mapper 接口能够直接调用,靠的是动态代理。
Spring 启动时,MyBatis 扫描 Mapper 接口,通过 MapperFactoryBean 将其注册为 Bean。真正注入业务类的不是接口实现类,而是由 MapperProxyFactory 创建的代理对象。
当我们调用:
userMapper.findById(1L);
请求会进入 MapperProxy。代理对象根据“接口全限定名 + 方法名”找到对应的 MappedStatement,然后通过 SqlSession 执行查询。
后续可以概括为下面这条链路:
Mapper 接口方法
→ MapperProxy
→ MapperMethod
→ SqlSession
→ Executor
→ StatementHandler
→ JDBC
→ ResultSetHandler
→ Java 对象
其中,Executor 负责执行策略和缓存,StatementHandler 负责创建并操作 JDBC Statement,ParameterHandler 负责设置参数,ResultSetHandler 则按照 resultType 或 resultMap 将结果集转换成 Java 对象。
网硕互联帮助中心
![基于SpringBoot的校园社团活动与经费管理系统[源码免费+文档免费]-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/08/20260829151713-6a92f7f9e7c60-220x150.png)





评论前必须登录!
注册