← 返回题目列表

MyBatis 执行一条 SQL 的完整流程是怎样的?

高频 困难 第 16 / 24 题 更新于 2026/07/26
MyBatisSqlSessionExecutorStatementHandler

简化版

Mapper 代理把方法调用交给 SqlSession,SqlSession 根据 statement id 取得 MappedStatement,再委托 Executor。Executor 生成 BoundSql、处理缓存并调用 StatementHandler,ParameterHandler 绑定参数,JDBC 执行后由 ResultSetHandler 把结果集映射成 Java 对象。

详细版

启动阶段先解析配置、Mapper XML 或注解,构建 Configuration 和 MappedStatement;运行阶段才根据参数生成最终 SQL。核心链路是 MapperProxy → SqlSession → Executor → StatementHandler → ParameterHandler/JDBC → ResultSetHandler,插件可代理 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的指定方法。

Executor 有 SIMPLE、REUSE、BATCH 类型;CachingExecutor 可在基础 Executor 外处理二级缓存。StatementHandler 根据 statementType 使用 PreparedStatement、普通 Statement 或 CallableStatement,常规参数化 SQL 主要走 PreparedStatementHandler。

完整版教学

一、启动阶段准备了什么

XMLConfigBuilder、XMLMapperBuilder 等解析配置与 Mapper,注册类型别名、TypeHandler、ResultMap、MappedStatement。MappedStatement 是一条映射语句的静态描述,不是执行后的 JDBC Statement。

动态 SQL 节点会保存为 SqlSource,运行时接收参数生成 BoundSql。BoundSql 才包含本次调用的 SQL 文本、参数映射和附加参数。

二、从 Mapper 到 Executor

MapperProxy → MapperMethod → SqlSession
                         → MappedStatement
                         → Executor.query/update

DefaultSqlSession 是对外门面,负责选择查询、更新等 Executor 操作。原生 SqlSession 不是线程安全对象,应按请求或事务正确关闭;Spring 中常用的 SqlSessionTemplate 则负责与当前事务绑定合适的会话。

阶段核心对象主要产物常见问题
启动解析Configuration、MappedStatementstatement id、SqlSource、ResultMapXML 没加载、namespace/id 不匹配
方法调用MapperProxy、MapperMethodSQL 命令类型、参数对象参数名绑定错误、返回类型不匹配
执行调度Executor缓存处理、事务委托一级/二级缓存误判、批处理未 flush
JDBC 准备StatementHandler、ParameterHandlerPreparedStatement、参数绑定SQL 语法、TypeHandler、jdbcType
结果映射ResultSetHandlerJava 对象、集合、关联对象列别名、ResultMap、N+1

记忆钩子:MappedStatement 是“题目配置”,BoundSql 是“本次试卷”,Executor 是“考务调度”,StatementHandler 才真正走向 JDBC。

三、Executor 的职责

Executor 负责一级缓存、事务相关委托、更新刷新以及创建 StatementHandler。SIMPLE 每次执行创建新 Statement,REUSE 尝试复用 PreparedStatement,BATCH 把多次更新批量提交给 JDBC 驱动。

启用并命中 namespace 二级缓存时,外层 CachingExecutor 可先处理二级缓存;没有命中才进入基础 Executor。缓存并不会改变后续 JDBC 执行链,只是可能让它被跳过。

四、StatementHandler 与参数绑定

RoutingStatementHandler 按映射配置选择具体 Handler。以 PreparedStatement 为例:

  1. prepare 创建并配置 JDBC PreparedStatement;
  2. ParameterHandler 根据 ParameterMapping 和 TypeHandler 调用 setXxx;
  3. StatementHandler 执行 query 或 update;
  4. ResultSetHandler 处理结果。

#{} 最终对应 ? 占位符,TypeHandler 决定 Java 值如何设置到 JDBC、以及数据库值如何读取回来。

Mapper.selectById(7)
  -> statementId = com.example.UserMapper.selectById
  -> SqlSource + 参数 7 生成 BoundSql: SELECT ... WHERE id = ?
  -> ParameterMapping: id
  -> TypeHandler: LongTypeHandler.setLong(ps, 1, 7)
  -> JDBC executeQuery
  -> ResultSetHandler 映射 User

这个流程里,#{id} 不是简单替换成 7,而是变成 ? 加一条参数映射。理解这一点,就能解释为什么 #{} 防注入、为什么 ${} 会改变 SQL 文本、为什么参数个数和占位符个数必须一致。

五、结果如何变成对象

ResultSetHandler 根据 resultType 或 ResultMap 创建结果对象,匹配列与属性,调用 TypeHandler 读取值。关联、集合、构造器映射、判别器和延迟加载也在结果映射阶段参与。

映射错误时应查看实际列名、别名、ResultMap、驼峰配置与 TypeHandler,而不是只检查 SQL 能否在数据库客户端执行。

一个常见排查例子:数据库客户端执行 SELECT user_name FROM users 没问题,但 Java 对象的 userName 是 null。这时应该沿结果映射层排查:列名是否有别名、是否开启 mapUnderscoreToCamelCase、resultMap 是否写成了 column="username"、是否有自定义 TypeHandler 把值处理掉。SQL 正确只说明 JDBC 能拿到列,不代表 MyBatis 一定能把列放到目标属性上。

反过来,如果报参数绑定异常,如 “Parameter ‘id’ not found”,问题通常还没走到数据库执行,而是在 MapperMethod 参数解析、BoundSql 参数映射或 ParameterHandler 绑定阶段。把错误定位到链路层级,能显著减少无效排查。

六、插件在哪一层生效

MyBatis 插件不是任意方法 AOP,只能拦截官方允许的四类对象及指定方法。分页插件常在 StatementHandler prepare 前改写 SQL,也有插件在 Executor query 层处理;拦截点不同,拿到的对象与缓存影响也不同。

如果分页插件在 StatementHandler 层把 SQL 改成 SELECT ... LIMIT ?, ?,它就必须确保新增的两个占位符有对应参数映射;如果在 Executor 层处理,则更容易同时接触 MappedStatement、RowBounds 和 CacheKey。不同层的优缺点不一样,所以成熟插件一般会明确说明自己拦截哪一层,而不是只说“执行前改 SQL”。

七、常见误区与追问

  • 误区:Mapper 方法调用时才临时扫描 XML。 XML 和注解在启动解析阶段已经注册为 MappedStatement,运行时按 statement id 查找。
  • 误区:MappedStatement 就是最终 SQL。 MappedStatement 是静态配置,动态 SQL 要结合本次参数生成 BoundSql 才是最终 SQL 文本和参数映射。
  • 追问:Executor 的三种类型怎么区分? SIMPLE 每次创建 Statement,REUSE 复用 Statement,BATCH 把更新加入 JDBC batch 后统一 flush。
  • 追问:参数绑定在哪一步完成? ParameterHandler 根据 BoundSql 的 ParameterMapping 和 TypeHandler 给 PreparedStatement 设置参数。
  • 误区:SQL 能在客户端执行就说明 MyBatis 映射没问题。 MyBatis 还涉及参数名、TypeHandler、列别名、ResultMap 和缓存层,问题可能不在 SQL 文本本身。
  • 追问:插件为什么不能拦截任意业务方法? MyBatis 插件只代理 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的指定方法,不是通用 Spring AOP。

八、加强记忆

启动时把 XML/注解变成 MappedStatement,运行时把参数变成 BoundSql;调用主线是 MapperProxy → SqlSession → Executor → StatementHandler,ParameterHandler 负责入参,ResultSetHandler 负责出参。排错时先判断问题属于哪一层。