云计算百科
云计算领域专业知识百科平台

MyBatis 核心原理详解:JPA、二级缓存、结果映射与 Mapper 代理

前言

刚接触 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 对象。

赞(0)
未经允许不得转载:网硕互联帮助中心 » MyBatis 核心原理详解:JPA、二级缓存、结果映射与 Mapper 代理
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!