0%

mybatis的sql节点

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
2
3
4
public interface SqlSource {
// 根据参数对象,生成包含最终SQL和参数映射的BoundSql
BoundSql getBoundSql(Object parameterObject);
}

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>
工作原理:
  1. 初始化阶段:解析动态标签,生成SqlNode树(每个动态标签对应一个SqlNode实现类,如<if>对应IfSqlNode);
  2. 执行阶段(调用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}会转为?占位符)

工作原理:
  1. 初始化阶段:
    • 解析 SQL 中的${}占位符,替换为实际参数(或保留为占位符,等待执行时替换);
    • 处理静态文本,生成包含?占位符的 SQL 语句;
    • 直接转换为StaticSqlSource(后续无需再解析)。
  2. 执行阶段:直接调用内部StaticSqlSourcegetBoundSql方法生成BoundSql
关键区别(与 DynamicSqlSource):
  • 解析时机:RawSqlSource初始化阶段完成解析,DynamicSqlSource执行阶段解析;
  • 性能:RawSqlSource性能更高(只需解析一次),适合高频执行的静态 SQL。

3. StaticSqlSource:最终可执行的静态 SQL

核心职责:存储完全解析后的静态 SQL(仅包含?占位符,无动态逻辑),可直接用于生成PreparedStatement

适用场景:
  • 经过DynamicSqlSourceRawSqlSource解析后的最终 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 语句固定不变(无动态逻辑),性能最优;
  • DynamicSqlSourceRawSqlSource的最终产物(二者解析后均转为StaticSqlSource)。

4. ProviderSqlSource:注解式 SQL 的处理器

核心职责:处理通过注解(如@SelectProvider/@InsertProvider)定义的 SQL,从指定类的方法中获取 SQL 字符串。

适用场景:
  • 注解式 Mapper 中通过 Provider 类生成 SQL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface UserMapper {
    @SelectProvider(type = UserSqlProvider.class, method = "selectUser")
    User selectUser(@Param("id") Long id);
    }

    // SQL提供类
    public class UserSqlProvider {
    public String selectUser(Long id) {
    return "SELECT * FROM user WHERE id = #{id}";
    }
    }
工作原理:
  1. 初始化阶段:记录 Provider 类(type)和方法(method)的元信息;
  2. 执行阶段:
    • 调用 Provider 类的指定方法,传入参数,获取生成的 SQL 字符串;
    • 将 SQL 字符串解析为StaticSqlSource(若包含动态逻辑,可能转为DynamicSqlSource);
    • 最终通过解析后的SqlSource生成BoundSql
关键特点:
  • 支持通过 Java 代码动态生成 SQL(比 XML 动态标签更灵活);
  • 解析时机取决于生成的 SQL 是否包含动态逻辑(静态则初始化解析,动态则执行时解析)。

总结:四大 SqlSource 的关系与流转

1
2
3
4
5
XML/注解SQL节点
├─ 含动态标签(<if>等)→ DynamicSqlSource → 执行时解析 → StaticSqlSource → BoundSql
├─ 静态SQL含${} → RawSqlSource → 初始化解析 → StaticSqlSource → BoundSql
├─ 纯静态SQL(仅#{})→ StaticSqlSource → 直接生成 BoundSql
└─ 注解@XxxProvider → ProviderSqlSource → 调用方法生成SQL → 转为上述三种之一 → BoundSql

BoundSql:SQL 执行的 “最终蓝图”

BoundSqlSqlSource解析的产物,封装了执行 SQL 所需的全部信息,是连接 SQL 解析与数据库执行的关键对象。

BoundSql 类结构

1
2
3
4
5
6
7
public class BoundSql {
private final String sql; // 最终可执行的SQL语句(含?占位符)
private final List<ParameterMapping> parameterMappings; // 参数映射列表
private final Object parameterObject; // 传入的参数对象
private final Map<String, Object> additionalParameters; // 额外参数(如分页参数)
private final MetaObject metaParameters; // 用于操作additionalParameters的工具
}

核心属性解析

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,分别对应nameage参数。

3. parameterObject:实际传入的参数
  • 可能的类型:
    • 简单类型(Long/String等,单参数时);
    • 实体类对象(多参数封装为对象时);
    • Map对象(多参数未封装时,MyBatis 自动转为Map,键为@Param指定的名称或参数索引)。
4. additionalParameters 与 metaParameters
  • additionalParameters:存储额外参数(如分页插件添加的offset/limit);
  • metaParameters:基于MetaObject工具类,提供对additionalParameters的便捷访问(如setValue/getValue)。

BoundSql 的生成与使用流程

  1. 生成SqlSource.getBoundSql(parameterObject)根据参数对象生成BoundSql
  2. 参数绑定ParameterHandler使用parameterMappingsparameterObject的属性值绑定到PreparedStatement?占位符;
  3. 执行StatementHandler使用BoundSql中的sql创建PreparedStatement,执行并返回结果。

实战:SqlSource 与 BoundSql 的调试与问题排查

1. 查看解析后的 SQL 与参数映射

通过拦截器打印BoundSql信息,可快速定位 SQL 解析问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class SqlDebugInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");

// 打印解析后的SQL
System.out.println("SQL: " + boundSql.getSql());
// 打印参数映射
System.out.println("Parameters: " + boundSql.getParameterMappings());
// 打印参数对象
System.out.println("ParameterObject: " + boundSql.getParameterObject());

return invocation.proceed();
}
}

2. 常见问题及原因

问题现象 可能原因 关联组件
SQL 语法错误(如缺少关键字) 动态标签拼接错误(如<if>条件始终为真导致重复 AND) DynamicSqlSource
参数绑定错误(参数类型不匹配) parameterMappingsjavaTypetypeHandler配置错误 BoundSql.parameterMappings
SQL 注入风险 使用${}替换敏感参数(如表名 / 列名)未做过滤 RawSqlSource/DynamicSqlSource
性能低下(高频执行的 SQL) 动态 SQL 解析耗时(可转为静态 SQL) DynamicSqlSource

总结:SQL 解析体系的核心价值

MyBatis 的SqlSourceBoundSql体系通过分层设计与职责分离,实现了对各类 SQL 的灵活处理:

  • SqlSource接口统一了 SQL 的解析入口,屏蔽了静态与动态 SQL 的差异;
  • 四大实现类分工明确,覆盖了 XML 静态 SQL、动态 SQL、注解 SQL 等场景;
  • BoundSql作为最终产物,整合了执行 SQL 所需的全部信息,简化了后续执行流程

欢迎关注我的其它发布渠道