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树; - 最终封装成
DynamicSqlSource或RawSqlSource。
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 缓冲区 |
| 表达式解析 | 使用 OGNL 或 EL 表达式解析 <if test="..."> |
| 节点扩展性 | 通过实现 SqlNode 接口可扩展任意自定义标签 |

1092

被折叠的 条评论
为什么被折叠?



