跳过正文
  1. 博客/

MyBatis SQL 执行流程

·387 字·2 分钟

MyBatis 一条 SQL 的执行之旅
#

从 Mapper 接口说起
#

用 MyBatis 写代码,日常就是定义一个 Mapper 接口,写个 XML 或注解,然后调用方法就完事了。但你有没有想过,你调 userMapper.selectById(1) 的时候,MyBatis 背后到底干了啥?

我之前一直觉得这东西就是个黑盒,SQL 进去,对象出来,不用管就好。直到有次面试被问"说说 MyBatis 的执行流程",我直接愣住了。回来之后认真翻了源码,发现整个流程其实挺清晰的,今天来捋一捋。

简单说,你调 Mapper 方法,其实调的是一个 JDK 动态代理对象。MyBatis 在启动的时候会给每个 Mapper 接口生成代理,拦截方法调用,然后转到 SqlSession 去执行。

// 你以为你在调接口方法
User user = userMapper.selectById(1);

// 实际上等价于
User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectById", 1);

所以 Mapper 接口本身没有实现类,全靠代理。这个设计挺巧妙的,让你写代码的时候感觉像在调普通方法。

SqlSession 是什么
#

SqlSession 是 MyBatis 的核心接口,相当于一次数据库会话。它提供了 selectOne、selectList、insert、update、delete 这些方法。

不过 SqlSession 自己不干活,它把事情委托给 Executor。你可以把 SqlSession 理解成一个门面,真正干活的是后面的 Executor。

public class DefaultSqlSession implements SqlSession {
    private final Executor executor;
    
    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        }
        // ...
    }
}

话说回来,如果你用的是 Spring + MyBatis,SqlSession 的创建和关闭都被 SqlSessionTemplate 管了,你基本不用操心。

Executor 执行器
#

Executor 是真正执行 SQL 的组件。MyBatis 提供了三种 Executor:

  • SimpleExecutor:每次执行都创建新的 Statement,最朴素的方式
  • ReuseExecutor:会复用 Statement,减少创建开销
  • BatchExecutor:批量执行,适合大量 insert/update 的场景

默认用的是 SimpleExecutor。Executor 拿到 MappedStatement(就是你 XML 里那条 SQL 的所有配置信息),然后交给 StatementHandler 去处理。

// Executor 的核心方法
public <E> List<E> query(MappedStatement ms, Object parameter, 
                          RowBounds rowBounds, ResultHandler resultHandler) {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

注意这里有个 CacheKey,后面讲缓存会用到。

StatementHandler 和参数映射
#

StatementHandler 负责创建 JDBC 的 Statement,设置参数,执行 SQL。

参数设置这块是 ParameterHandler 干的。它会把你传的 Java 对象映射成 SQL 的参数。这里用到了 TypeHandler,比如把 Java 的 String 映射成 VARCHAR,把 Date 映射成 TIMESTAMP。

// ParameterHandler 设置参数的核心逻辑
public void setParameters(PreparedStatement ps) {
    for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        Object value = // 从参数对象中取值
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
    }
}

我之前踩过一个坑:参数是 Map 的时候,XML 里的 #{key} 要和 Map 的 key 对上,不然就是 null。debug 了半天才发现是 key 写错了,大小写不一致。这种问题 MyBatis 不会报错,就默默给你传个 null,很坑。

结果集映射
#

SQL 执行完,ResultSetHandler 负责把 JDBC 的 ResultSet 映射成 Java 对象。

这个过程大概是:

  1. 根据 resultMap 或 resultType 确定目标类型
  2. 创建目标对象(通过反射)
  3. 遍历每一列,用 TypeHandler 把数据库类型转成 Java 类型
  4. 通过反射设置属性值

如果你用了 resultMap 还配了 association 和 collection,那就涉及嵌套查询或嵌套结果集映射,逻辑会复杂不少。

顺便提一下,MyBatis 的自动映射(autoMapping)默认是开的,它会尝试把列名和属性名匹配。驼峰命名转换可以通过 mapUnderscoreToCamelCase 配置开启,这个基本是必开的,不然数据库的 user_name 映射不到 Java 的 userName

一级缓存和二级缓存
#

MyBatis 有两级缓存,面试也爱问这个。

一级缓存在 SqlSession 级别,默认开启。同一个 SqlSession 里,相同的查询只会执行一次 SQL,第二次直接从缓存拿。

// 同一个 SqlSession 内
User user1 = sqlSession.selectOne("selectById", 1); // 走数据库
User user2 = sqlSession.selectOne("selectById", 1); // 走缓存
// user1 == user2   true同一个对象

但是有个坑:在 Spring 里,默认每个方法调用都是新的 SqlSession,所以一级缓存其实没啥用。除非你在同一个事务里多次查询,才能命中。

二级缓存在 namespace 级别(也就是 Mapper 级别),需要手动开启。多个 SqlSession 可以共享。

<!-- 在 Mapper XML 里加这个就开了 -->
<cache />

二级缓存听起来不错,但实际项目里很少用。原因是它的粒度太粗——整个 namespace 共享一个缓存,一有更新操作就全部失效。而且多表关联查询的时候,更新了 A 表但 B 的 Mapper 缓存不会失效,容易出脏数据。

其实吧,缓存这事别靠 MyBatis,老老实实用 Redis 更靠谱。

整体流程串一下
#

把整个流程串起来:

  1. 调用 Mapper 接口方法(动态代理拦截)
  2. SqlSession 接收调用
  3. Executor 执行查询(先查缓存)
  4. StatementHandler 创建 Statement
  5. ParameterHandler 设置参数
  6. 执行 SQL
  7. ResultSetHandler 映射结果
  8. 返回 Java 对象

说到这里,MyBatis 的设计还是挺清晰的,每个组件职责分明。理解了这个流程,看源码也不会太懵。面试的时候把这条链路讲清楚,基本就 OK 了。

有兴趣的话可以自己打断点跟一遍,从 MapperProxy.invoke() 开始,一路跟到 JDBC 执行,印象会深刻很多。