MyBatis SQL 节点解析:从 SqlSource 到 BoundSql 的完整流程
MyBatis 中 SQL 节点(<select>/<insert>等)的解析是 SQL 执行的基础环节。从 Mapper 配置文件中的 SQL 文本,到最终可执行的数据库语句,中间经历了 “SQL 节点→SqlSource→BoundSql” 的转换过程。本文将深入解析SqlSource接口及其实现类的职责,以及BoundSql的核心作用,揭示 MyBatis 处理静态与动态 SQL 的底层逻辑。
SqlSource:SQL 语句的 “源容器”
SqlSource是 MyBatis 中封装 SQL 语句及其解析逻辑的接口,它的核心职责是根据传入的参数对象,生成可执行的BoundSql对象(包含最终 SQL、参数映射等信息)。
SqlSource 接口定义
1 | public interface SqlSource { |
SqlSource的关键作用是隔离 SQL 的静态结构与动态解析逻辑:无论是静态 SQL(无动态标签)还是动态 SQL(含<if>/<foreach>等),都通过统一的getBoundSql方法提供可执行的 SQL 信息,简化了后续执行器(Executor)的处理逻辑。
SqlSource 的四大实现类:分工与场景
MyBatis 提供 4 种SqlSource实现类,分别对应不同类型的 SQL 定义(静态 / 动态 / 注解提供等),形成了完整的 SQL 解析体系。
1. DynamicSqlSource:动态 SQL 的解析器
核心职责:处理包含动态标签(<if>/<when>/<foreach>/<trim>等)的 SQL 语句,需要根据参数动态生成最终 SQL。
适用场景:
包含动态逻辑的 SQL(如 “根据条件拼接查询条件”):
1
2
3
4
5
6
7<select id="selectUser" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</where>
</select>
工作原理:
- 初始化阶段:解析动态标签,生成
SqlNode树(每个动态标签对应一个SqlNode实现类,如<if>对应IfSqlNode); - 执行阶段(调用getBoundSql时):
- 根据参数对象(
parameterObject)评估动态条件(如<if test="name != null">); - 拼接满足条件的 SQL 片段,生成最终包含
?占位符的 SQL 语句; - 将结果封装为
StaticSqlSource,再通过其生成BoundSql。
- 根据参数对象(
关键特点:
- 动态解析延迟到执行阶段(每次调用都可能生成不同 SQL);
- 依赖
SqlNode树和参数对象动态生成 SQL,性能略低于静态 SQL。
2. RawSqlSource:预解析的静态 SQL 处理器
核心职责:处理无动态标签但可能包含${}占位符的静态 SQL,在初始化阶段完成解析,生成StaticSqlSource。
适用场景:
无动态标签,但包含${}(文本替换)的 SQL:
1
2
3<select id="selectUserByTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>(注:${tableName}会被直接替换为参数值,#{id}会转为?占位符)
工作原理:
- 初始化阶段:
- 解析 SQL 中的
${}占位符,替换为实际参数(或保留为占位符,等待执行时替换); - 处理静态文本,生成包含
?占位符的 SQL 语句; - 直接转换为
StaticSqlSource(后续无需再解析)。
- 解析 SQL 中的
- 执行阶段:直接调用内部
StaticSqlSource的getBoundSql方法生成BoundSql。
关键区别(与 DynamicSqlSource):
- 解析时机:
RawSqlSource在初始化阶段完成解析,DynamicSqlSource在执行阶段解析; - 性能:
RawSqlSource性能更高(只需解析一次),适合高频执行的静态 SQL。
3. StaticSqlSource:最终可执行的静态 SQL
核心职责:存储完全解析后的静态 SQL(仅包含?占位符,无动态逻辑),可直接用于生成PreparedStatement。
适用场景:
经过
DynamicSqlSource或RawSqlSource解析后的最终 SQL;完全静态的 SQL(无任何动态标签和${}占位符):
1
2
3<select id="selectUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
工作原理:
- 内部存储最终的 SQL 字符串(如
SELECT * FROM user WHERE id = ?)和参数映射列表(parameterMappings); - 调用
getBoundSql时,直接基于存储的 SQL 和参数映射,生成BoundSql对象。
关键特点:
- SQL 语句固定不变(无动态逻辑),性能最优;
- 是
DynamicSqlSource和RawSqlSource的最终产物(二者解析后均转为StaticSqlSource)。
4. ProviderSqlSource:注解式 SQL 的处理器
核心职责:处理通过注解(如@SelectProvider/@InsertProvider)定义的 SQL,从指定类的方法中获取 SQL 字符串。
适用场景:
注解式 Mapper 中通过 Provider 类生成 SQL:
1
2
3
4
5
6
7
8
9
10
11public interface UserMapper {
User selectUser( Long id);
}
// SQL提供类
public class UserSqlProvider {
public String selectUser(Long id) {
return "SELECT * FROM user WHERE id = #{id}";
}
}
工作原理:
- 初始化阶段:记录 Provider 类(
type)和方法(method)的元信息; - 执行阶段:
- 调用 Provider 类的指定方法,传入参数,获取生成的 SQL 字符串;
- 将 SQL 字符串解析为
StaticSqlSource(若包含动态逻辑,可能转为DynamicSqlSource); - 最终通过解析后的
SqlSource生成BoundSql。
关键特点:
- 支持通过 Java 代码动态生成 SQL(比 XML 动态标签更灵活);
- 解析时机取决于生成的 SQL 是否包含动态逻辑(静态则初始化解析,动态则执行时解析)。
总结:四大 SqlSource 的关系与流转
1 | XML/注解SQL节点 |
BoundSql:SQL 执行的 “最终蓝图”
BoundSql是SqlSource解析的产物,封装了执行 SQL 所需的全部信息,是连接 SQL 解析与数据库执行的关键对象。
BoundSql 类结构
1 | public class BoundSql { |
核心属性解析
1. sql:最终执行的 SQL 语句
- 内容:经过解析后的 SQL 字符串,其中
#{}被替换为?(预编译占位符),${}被替换为实际参数值(文本替换); - 示例:
SELECT * FROM user WHERE name = ? AND age = ?。
2. parameterMappings:参数映射规则
ParameterMapping是参数的 “元数据”,描述如何将 Java 参数绑定到 SQL 的?占位符,核心属性包括:
property:参数对象的属性名(如name对应user.getName());javaType:参数的 Java 类型(如String);jdbcType:参数的 JDBC 类型(如VARCHAR);typeHandler:参数的类型处理器(如StringTypeHandler)。
示例:parameterMappings列表可能包含两个ParameterMapping,分别对应name和age参数。
3. parameterObject:实际传入的参数
- 可能的类型:
- 简单类型(
Long/String等,单参数时); - 实体类对象(多参数封装为对象时);
Map对象(多参数未封装时,MyBatis 自动转为Map,键为@Param指定的名称或参数索引)。
- 简单类型(
4. additionalParameters 与 metaParameters
additionalParameters:存储额外参数(如分页插件添加的offset/limit);metaParameters:基于MetaObject工具类,提供对additionalParameters的便捷访问(如setValue/getValue)。
BoundSql 的生成与使用流程
- 生成:
SqlSource.getBoundSql(parameterObject)根据参数对象生成BoundSql; - 参数绑定:
ParameterHandler使用parameterMappings将parameterObject的属性值绑定到PreparedStatement的?占位符; - 执行:
StatementHandler使用BoundSql中的sql创建PreparedStatement,执行并返回结果。
实战:SqlSource 与 BoundSql 的调试与问题排查
1. 查看解析后的 SQL 与参数映射
通过拦截器打印BoundSql信息,可快速定位 SQL 解析问题:
1 |
|
2. 常见问题及原因
| 问题现象 | 可能原因 | 关联组件 |
|---|---|---|
| SQL 语法错误(如缺少关键字) | 动态标签拼接错误(如<if>条件始终为真导致重复 AND) |
DynamicSqlSource |
| 参数绑定错误(参数类型不匹配) | parameterMappings的javaType或typeHandler配置错误 |
BoundSql.parameterMappings |
| SQL 注入风险 | 使用${}替换敏感参数(如表名 / 列名)未做过滤 |
RawSqlSource/DynamicSqlSource |
| 性能低下(高频执行的 SQL) | 动态 SQL 解析耗时(可转为静态 SQL) | DynamicSqlSource |
总结:SQL 解析体系的核心价值
MyBatis 的SqlSource与BoundSql体系通过分层设计与职责分离,实现了对各类 SQL 的灵活处理:
SqlSource接口统一了 SQL 的解析入口,屏蔽了静态与动态 SQL 的差异;- 四大实现类分工明确,覆盖了 XML 静态 SQL、动态 SQL、注解 SQL 等场景;
BoundSql作为最终产物,整合了执行 SQL 所需的全部信息,简化了后续执行流程