MyBatis 的 动态 SQL

MyBatis 的 动态 SQL 是其核心功能之一,它允许我们根据业务逻辑灵活地构建 SQL 语句。常见的标签如 <if><choose><when><otherwise><trim><where><set><foreach> 等,都属于 MyBatis 动态 SQL 的一部分。


🧠 一、动态 SQL 的核心类

org.apache.ibatis.scripting.xmltags
├── SqlNode.java                // SQL 节点接口
├── MixedSqlNode.java           // 多个子节点的组合
├── TextSqlNode.java            // 静态文本节点(包含 ${})
├── StaticTextSqlNode.java      // 完全静态的 SQL 文本
├── IfSqlNode.java              // <if> 标签实现
├── ChooseSqlNode.java          // <choose>/<when>/<otherwise> 实现
├── TrimSqlNode.java            // <trim> 实现
├── WhereSqlNode.java           // <where> 实现
├── SetSqlNode.java             // <set> 实现
├── ForEachSqlNode.java         // <foreach> 实现
├── VarDeclSqlNode.java         // <bind> 标签实现
└── XMLScriptBuilder.java       // 构建动态 SQL 节点树

📦 二、SQL 解析流程

1. 加载阶段:XML 解析为 MappedStatement

当加载 mapper.xml 文件时:

  • 使用 XMLMapperBuilder 解析 <select><update> 等 SQL 标签;
  • 将动态 SQL 转换为一棵 SqlNode 树;
  • 最终封装成 DynamicSqlSourceRawSqlSource

2. 执行阶段:运行时拼接 SQL 字符串

  • 在调用 SQL 时,通过 DynamicSqlSource.getBoundSql() 方法生成最终的 SQL;
  • 遍历 SqlNode.apply(),递归拼接 SQL 片段;
  • 参数处理由 ParameterHandler 完成(支持 #{}${})。

🔍 三、关键标签实现

✅ 1. <if test="...">

示例:
<select id="selectUsers" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>
源码实现:IfSqlNode
public class IfSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String test;
    private final SqlNode contents;

    public boolean apply(DynamicContext context) {
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context);
            return true;
        }
        return false;
    }
}
  • test 表达式通过 OGNL 或 EL 表达式引擎解析;
  • 如果表达式为真,则将内部内容写入 DynamicContext

✅ 2. <choose>/<when>/<otherwise>

示例:
<choose>
    <when test="id != null">
        id = #{id}
    </when>
    <when test="name != null">
        name = #{name}
    </when>
    <otherwise>
        status = 1
    </otherwise>
</choose>
源码实现:ChooseSqlNode
public class ChooseSqlNode implements SqlNode {
    private final List<SqlNode> whenSqlNodes;
    private final SqlNode otherwiseSqlNode;

    public boolean apply(DynamicContext context) {
        for (SqlNode sqlNode : whenSqlNodes) {
            if (sqlNode.apply(context)) {
                return true;
            }
        }
        if (otherwiseSqlNode != null) {
            otherwiseSqlNode.apply(context);
        }
        return true;
    }
}
  • 依次尝试每个 <when>,一旦命中则不再继续;
  • 若都没有命中,使用 <otherwise>

✅ 3. <where>

示例:
<where>
    <if test="name != null">AND name = #{name}</if>
    <if test="age != null">AND age = #{age}</if>
</where>
源码实现:WhereSqlNode
public class WhereSqlNode extends TrimSqlNode {

    private static final List<String> prefixToOverride = Arrays.asList("AND ", "OR ", "AND/OR ", "OR/AND ", " ");

    public WhereSqlNode(Configuration configuration, SqlNode contents) {
        super(configuration, contents, "WHERE", prefixToOverride, null, null);
    }
}
  • 继承自 TrimSqlNode
  • 自动去除开头的 AND / OR,并添加 WHERE 前缀。

✅ 4. <set>

示例:
<update id="updateUser">
    UPDATE user
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="age != null">age = #{age}</if>
    </set>
    WHERE id = #{id}
</update>
源码实现:SetSqlNode
public class SetSqlNode extends TrimSqlNode {

    private static final List<String> suffixToOverride = Arrays.asList(",", ",");

    public SetSqlNode(Configuration configuration, SqlNode contents) {
        super(configuration, contents, "SET", null, suffixToOverride, null);
    }
}
  • 同样继承自 TrimSqlNode
  • 自动去除结尾的逗号,并添加 SET 前缀。

✅ 5. <trim>

示例:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
</trim>
源码实现:TrimSqlNode
public class TrimSqlNode implements SqlNode {
    private final SqlNode contents;
    private final String prefix;
    private final String suffix;
    private final List<String> prefixesToOverride;
    private final List<String> suffixesToOverride;

    public boolean apply(DynamicContext context) {
        GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
            DynamicContext oldContext = context;
            context = new PrefixedContext(oldContext, prefix, suffix, prefixesToOverride, suffixesToOverride);
            contents.apply(context);
            context = oldContext;
            return "";
        });
        parser.parse(content);
        return true;
    }
}
  • 可以控制前缀、后缀,以及需要忽略的前后缀字符串。

✅ 6. <foreach>

示例:
<foreach item="id" collection="ids" open="(" separator="," close=")">
    #{id}
</foreach>
源码实现:ForEachSqlNode
public class ForEachSqlNode implements SqlNode {
    private final String collectionItem;
    private final String collection;
    private final String separator;
    private final String open;
    private final String close;
    private final SqlNode contents;

    public boolean apply(DynamicContext context) {
        Map<String, Object> bindings = context.getBindings();
        final Iterable<?> iterable = resolveIterable(bindings);
        Iterator<?> iterator = iterable.iterator();

        if (iterator.hasNext()) {
            context.appendSql(open);
            int i = 0;
            while (iterator.hasNext()) {
                Object item = iterator.next();
                context.bind(collectionItem, item);
                contents.apply(context);
                if (i < size - 1) {
                    context.appendSql(separator);
                }
                i++;
            }
            context.appendSql(close);
        }
        return true;
    }
}
  • 支持集合、数组、Map、List 等类型;
  • 会遍历元素并绑定到上下文;
  • 支持 open, close, separator 控制格式。

🧪 四、调试

1. 设置断点观察 SQL 构建过程

  • DynamicSqlSource.getBoundSql() 进入;
  • 查看 MixedSqlNode.apply() 如何递归构建 SQL;
  • 观察 DynamicContext 中的 sqlBuffer 内容。

2. 使用日志查看最终 SQL

  • 开启 MyBatis 日志(log4j / logback);
  • 输出类似如下信息:
==>  Preparing: SELECT * FROM user WHERE name = ? AND age = ?
==> Parameters: name=John(String), age=25(Integer)

🎯 五、总结

技术点说明
SQL 构建模型抽象语法树(AST)方式构建 SQL
上下文管理使用 DynamicContext 保存参数和 SQL 缓冲区
表达式解析使用 OGNLEL 表达式解析 <if test="...">
节点扩展性通过实现 SqlNode 接口可扩展任意自定义标签

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值