MyBatis 为什么知道该执行哪条 SQL?

上一篇我们讲了:

Mapper 接口为什么没有实现类,却能执行 SQL?

答案是:

MyBatis 在运行期间,通过动态代理创建了 Mapper 的代理对象。

但新的问题来了。

假设我们执行:

userMapper.selectById(1L);

代理对象确实接管了方法调用。

可是它怎么知道应该执行哪条 SQL?

毕竟项目里可能有:

UserMapper

OrderMapper

ProductMapper

...

几百个 Mapper。

几千条 SQL。

MyBatis 是如何准确找到对应 SQL 的?


先思考一个问题

假设让你设计 MyBatis。

看到下面这个接口:

@Mapper
public interface UserMapper {

    User selectById(Long id);

}

以及 XML:

<select id="selectById">
    select * from user where id = #{id}
</select>

当执行:

userMapper.selectById(1L);

你怎么找到对应 SQL?

最简单的方法:

方法名
      ↓
SQL

例如:

selectById
      ↓
select * from user where id = #{id}

看起来没问题,但很快就会发现问题。


方法名可能重复

例如:

UserMapper.selectById()
OrderMapper.selectById()
ProductMapper.selectById()

都有:

selectById

如果只用方法名,根本无法区分。


MyBatis 的解决方案

MyBatis 给每个 SQL 分配了一个全局唯一 ID。

规则非常简单:

接口全限定名 + 方法名

例如:

com.demo.mapper.UserMapper

方法:

selectById

最终生成:

com.demo.mapper.UserMapper.selectById

这就是 MyBatis 内部真正使用的 SQL 标识。


XML 是怎么关联上的?

很多人以为:

<select id="selectById">

这里的 id 已经唯一了,其实不是。

真正解析后会变成:

namespace + id

例如:

<mapper namespace="com.demo.mapper.UserMapper">

    <select id="selectById">
        select * from user where id = #{id}
    </select>

</mapper>

最终得到:

com.demo.mapper.UserMapper.selectById

是不是和刚才代理对象拿到的一模一样?

这样映射关系就建立起来了。


MyBatis 启动时干了什么?

项目启动时。

MyBatis 会解析所有 Mapper XML。

例如:

<select id="selectById">

解析后封装成:

MappedStatement

对象。

你可以理解成:

SQL配置对象

里面保存:

SQL内容

参数信息

返回值信息

缓存配置

执行类型

等等。

最终放进一个大 Map:

Configuration

里面。

类似:

Map<String, MappedStatement>

结构:

com.demo.mapper.UserMapper.selectById
        ↓
MappedStatement

com.demo.mapper.OrderMapper.selectById
        ↓
MappedStatement

代理对象是怎么找到 SQL 的?

当执行:

userMapper.selectById(1L);

进入:

MapperProxy.invoke()

以后。

MyBatis 会拿到:

method.getDeclaringClass()

以及:

method.getName()

拼出:

com.demo.mapper.UserMapper.selectById

然后直接去:

Configuration

查找:

MappedStatement ms =
configuration.getMappedStatement(statementId);

找到以后。

整个 SQL 信息就拿到了。


为什么要设计 MappedStatement?

很多人第一次看源码都会疑惑。

为什么不直接存 SQL?

例如:

select * from user where id = ?

不就够了吗?

其实远远不够。

MyBatis 执行 SQL 时需要的信息很多:

SQL内容、参数类型、返回值类型、缓存配置、动态SQL配置、ResultMap

TypeHandler

所以 MyBatis 设计了:

MappedStatement

统一保存所有 SQL 元数据。

后面整个执行链路都围绕它展开。


看一眼源码

解析 XML 时:

XMLMapperBuilder

负责读取:

<select>

<insert>

<update>

<delete>

最终创建:

MappedStatement

然后注册:

configuration.addMappedStatement(ms);

查询时:

configuration.getMappedStatement(id);

直接获取。

整个过程其实非常简单。

本质就是:

启动时建立映射

运行时快速查找

为什么这样设计?

如果每次执行 SQL 都解析 XML。

性能会非常差。

所以 MyBatis 的策略是:

启动时一次解析
       ↓
生成 MappedStatement
       ↓
缓存到 Configuration
       ↓
运行时直接获取

用空间换时间。

这也是大部分框架的经典设计。


总结

很多人以为:

userMapper.selectById()

执行时才去找 SQL。

实际上不是。

启动阶段。

MyBatis 已经把所有 SQL 解析完成。

并封装成:

MappedStatement

保存起来。

执行时只需要:

Mapper接口
      ↓
方法名
      ↓
生成 statementId
      ↓
找到 MappedStatement
      ↓
执行 SQL

所以:

MyBatis 之所以知道该执行哪条 SQL,本质上是因为启动时已经建立好了 Mapper 方法与 MappedStatement 的映射关系。


上一篇:

《Mapper 接口为什么没有实现类,却能执行 SQL?》

下一篇:

《一条 SQL 在 MyBatis 里到底经历了什么?》

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐