5258 字
约 17 分钟
1
第36篇-动态SQL与OGNL

第36篇-动态SQL与OGNL

来源: https://bugstack.cn/md/java/interview/2026-05-26-面经手册 · 第36篇《MyBatis 动态 SQL 怎么实现的?9个标签用法与 OGNL 解析原理》.html


面经手册 · 第36篇《MyBatis 动态 SQL 怎么实现的?9个标签用法与 OGNL 解析原理》

作者:小傅哥 **博客:https://bugstack.cn 沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

动态 SQL 是 MyBatis 最强大的特性之一,也是面试中出现频率极高的一道考题。 大部分候选人能说出 if、where、foreach 这几个标签的名字,但追问一句:where 标签是怎么自动去除前缀 AND/OR 的?foreach 的 collection 属性到底传什么?trim 和 where/set 是什么关系?动态 SQL 底层解析原理是什么?——大多数人就答不上来了。 本文从 9 个动态 SQL 标签的用法讲起,逐个给出完整代码示例,再深入源码追踪 OGNL 表达式解析和 SqlNode 树的遍历过程。不只是记住标签怎么用,更要知道底层是怎么跑的。

二、面试题

谢飞机,小记!,昨天刚背完 MyBatis 八股文,今天又来面试了。 面试官**:谢飞机,说说 MyBatis 动态 SQL 是什么?有哪些标签?

谢飞机:动态 SQL 就是根据条件拼接不同的 SQL。有 if、where、foreach、choose、set 这些标签。

面试官:if 标签的 test 属性用的是什么表达式语言?

谢飞机:OGNL... 吧?

面试官:where 标签是怎么自动去掉前缀 AND 的?

谢飞机:嗯... 它内部有个处理逻辑?

面试官:对,那这个逻辑在源码里怎么实现的?和 trim 标签什么关系?

谢飞机:trim... 我只知道它能去前缀后缀...

面试官:foreach 标签的 collection 属性,传 List 和传数组分别写什么?

谢飞机:list?array?

面试官:那传 Map 呢?key 是什么?

谢飞机:... 再见!ヾ( ̄▽ ̄)

三、9个动态SQL标签详解

1. if — 条件判断

最基础的动态 SQL 标签,根据条件决定是否拼接 SQL 片段。

select id="findUsers" resultType="User">
    SELECT * FROM user
    WHERE status = 'ACTIVE'
    if test="name != null">
        AND name LIKE CONCAT('%', #{name}, '%')
    if>
    if test="age != null">
        AND age = #{age}
    if>
select>

12 3 4 5 6 7 8 9 10 test 属性:使用 OGNL 表达式求值,结果为 true 时拼接 SQL 片段。

OGNL 常用判断写法:


if test="name != null and name != ''">

if test="list != null and list.size() > 0">

if test="ids != null and ids.size() > 0">

if test="name != null and name.indexOf('admin') >= 0">

1**2 3 4 5 6 7 8 9 10 11

2. choose/when/otherwise — 多条件分支

类似 Java 的 switch-case,只匹配第一个满足条件的分支。

select id="findUsers" resultType="User">
    SELECT * FROM user WHERE status = 'ACTIVE'
    choose>
        when test="id != null">
            AND id = #{id}
        when>
        when test="name != null">
            AND name = #{name}
        when>
        otherwise>
            AND create_time > '2024-01-01'
        otherwise>
    choose>
select>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 要点**:when 按顺序判断,匹配一个就跳出;otherwise 在所有 when 都不满足时生效。与多个 if 不同,choose 只会命中一个分支。

3. where — 智能WHERE子句

自动去除条件前缀的 AND/OR,避免语法错误。

select id="findUsers" resultType="User">
    SELECT * FROM user
    where>
        if test="name != null">
            AND name = #{name}
        if>
        if test="age != null">
            AND age = #{age}
        if>
    where>
select>

12 3 4 5 6 7 8 9 10 11 场景一:name 不为空、age 为空时,生成:

SELECT * FROM user WHERE name = ?

1AND 被自动去除。 场景二:name 和 age 都不为空时,生成:

SELECT * FROM user WHERE name = ? AND age = ?

1第一个 AND 被去除,第二个保留。 场景三:name 和 age 都为空时,生成:

SELECT * FROM user

1**WHERE 子句整体不生成。

4. set — 智能UPDATE SET子句

自动去除末尾多余的逗号。

update id="updateUser" parameterType="User">
    UPDATE user
    set>
        if test="name != null">
            name = #{name},
        if>
        if test="age != null">
            age = #{age},
        if>
        if test="email != null">
            email = #{email},
        if>
    set>
    WHERE id = #{id}
update>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 只更新非空字段,set 标签自动去除最后一个条件末尾的逗号。生成示例:

UPDATE user SET name = ?, age = ? WHERE id = ?

1

5. trim — 自定义前缀后缀裁剪

where 和 set 的底层实现都依赖 trim,trim 是最灵活的裁剪标签。


trim prefix="WHERE" prefixOverrides="AND |OR ">
    if test="name != null">
        AND name = #{name}
    if>
    if test="age != null">
        AND age = #{age}
    if>
trim>

trim prefix="SET" suffixOverrides=",">
    if test="name != null">
        name = #{name},
    if>
    if test="age != null">
        age = #{age},
    if>
trim>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 属性 说明 prefix 内容非空时,添加的前缀 suffix 内容非空时,添加的后缀 prefixOverrides 去除内容开头指定的字符串(多个用 suffixOverrides 去除内容末尾指定的字符串(多个用

6. foreach — 集合遍历

最常用的动态 SQL 标签之一,常用于 IN 查询和批量操作。 IN 查询**:

select id="findByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    foreach>
select>

12 3 4 5 6 批量插入

insert id="batchInsert">
    INSERT INTO user (name, age) VALUES
    foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age})
    foreach>
insert>

12 3 4 5 6 属性详解

属性 说明 示例
collection 遍历的集合对象 List→list,数组→array,Map→Map的key
item 每次迭代的变量名 #{id}
index 索引变量名(List为下标,Map为key) #{idx}
open 开始符号 (
close 结束符号 )
separator 每次迭代之间的分隔符 ,

collection 属性传值规则

// 1. 传 List:collection="list"(默认)或 @Param 指定名称
ListUser> findByIds(@Param("ids") ListLong> ids);  // collection="ids"

// 2. 传数组:collection="array"(默认)或 @Param 指定名称
ListUser> findByIds(@Param("ids") Long[] ids);  // collection="ids"

// 3. 传 Map:collection 为 Map 的 key
ListUser> findByMap(@Param("params") MapString, ListLong>> params);
// XML 中 collection="params.ids" 或直接传 Map 时 collection 为某个 key

1**2 3 4 5 6 7 8 9

7. bind — 变量绑定

在 SQL 解析阶段创建一个变量,供后续 OGNL 表达式引用。

select id="findUserByName" resultType="User">
    bind name="pattern" value="'%' + name + '%'" />
    SELECT * FROM user WHERE name LIKE #{pattern}
select>

1 2 3 4 典型场景**:模糊查询,避免使用 ${} 拼接通配符。bind 创建的变量可以用 #{} 引用,安全又灵活。

8. sql/include — SQL片段复用

将重复的 SQL 片段抽取出来,多处引用。


sql id="userColumns">
    id, name, age, email, create_time
sql>

select id="findUsers" resultType="User">
    SELECT include refid="userColumns"/> FROM user
select>

select id="findById" resultType="User">
    SELECT include refid="userColumns"/> FROM user WHERE id = #{id}
select>

12 3 4 5 6 7 8 9 10 11 12 13 注意:sql/include 不是动态 SQL 标签,它们在 MyBatis 解析阶段就完成了替换,不涉及运行时的条件判断。但日常开发中和动态 SQL 配合使用非常频繁。

四、OGNL表达式解析原理

1. 什么是OGNL

OGNL(Object-Graph Navigation Language)是一种表达式语言,用于读写 Java 对象的属性和调用方法。MyBatis 使用 OGNL 来解析动态 SQL 中 test 属性的表达式。


if test="name != null">
if test="list.size() > 0">
if test="type == 'admin'">
if test="age > 18 and age ">

1**2 3 4 5

2. OGNL在MyBatis中的执行流程

XML 映射文件中的 test 表达式
  ↓
OgnlCache.getValue(expression, context)
  ↓  编译表达式(缓存机制)
Ognl.getValue(parsedExpression, root)
  ↓  根据上下文参数求值
返回 true/false → 决定是否拼接 SQL 片段

1 2 3 4 5 6 7 OgnlCache 源码**:

// org.apache.ibatis.scripting.xmltags.OgnlCache
public class OgnlCache {

    private static final MapString, Object> expressionCache = new ConcurrentHashMap>();

    public static Object getValue(String expression, Object root) {
        try {
            // 编译表达式(有缓存)
            Object parsedExpression = parseExpression(expression);
            // 在 root 对象上求值
            return Ognl.getValue(parsedExpression, root);
        } catch (OgnlException e) {
            throw new RuntimeException("Error evaluating expression '" +
                expression + "'. Cause: " + e, e);
        }
    }

    private static Object parseExpression(String expression) throws OgnlException {
        // 先查缓存,没有再编译
        Object node = expressionCache.get(expression);
        if (node == null) {
            node = Ognl.parseExpression(expression);
            expressionCache.put(expression, node);
        }
        return node;
    }
}

12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 关键点:OGNL 表达式编译结果有缓存,同一条 SQL 多次执行时不会重复编译,性能有保障。

3. OGNL上下文中的参数访问

MyBatis 在构建 OGNL 上下文时,会将参数对象放入 _parameter 和方法参数名中:

// DynamicContext 构造时
bindings.put("_parameter", parameter);
bindings.put("_databaseId", databaseId);
// 如果是 @Param 注解的参数,还会放入具体的参数名

1**2 3 4 所以在 test 表达式中可以直接写属性名:


if test="name != null">

if test="name != null">

if test="param1 != null">

1 2 3 4 5 6 7 8

五、动态SQL源码解析

1. 解析入口:XMLScriptBuilder

MyBatis 解析 XML 映射文件中的 SQL 语句时,通过 XMLScriptBuilder 处理动态 SQL 标签:

// org.apache.ibatis.builder.xml.XMLScriptBuilder
public class XMLScriptBuilder extends BaseBuilder {

    public SqlSource parseScriptNode() {
        // 解析动态标签,构建 SqlNode 树
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

2. parseDynamicTags:构建SqlNode树

这是核心方法,递归解析 XML 节点,将每个标签转换为对应的 SqlNode 实现:

// org.apache.ibatis.builder.xml.XMLScriptBuilder
protected MixedSqlNode parseDynamicTags(XNode node) {
    ListSqlNode> contents = new ArrayList>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i  children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
            || child.getNode().getNodeType() == Node.TEXT_NODE) {
            // 文本节点
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {
                // 包含 ${} 的文本 → TextSqlNode
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                // 纯文本 → StaticTextSqlNode
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
            // 标签节点 → 根据标签名创建对应 SqlNode
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlers.get(nodeName);
            handler.handleNode(child, contents);
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

3. SqlNode体系

每个动态 SQL 标签都对应一个 SqlNode 实现类:

SqlNode(接口)
  ├── MixedSqlNode          — 组合节点,包含多个子 SqlNode
  ├── IfSqlNode             — 对应  标签
  ├── ChooseSqlNode         — 对应  标签
  ├── WhereSqlNode          — 对应  标签
  ├── SetSqlNode            — 对应  标签
  ├── TrimSqlNode           — 对应  标签
  ├── ForEachSqlNode        — 对应  标签
  ├── VarDeclSqlNode        — 对应  标签
  ├── TextSqlNode           — 包含 ${} 的文本
  └── StaticTextSqlNode     — 纯静态文本

1 2 3 4 5 6 7 8 9 10 11

4. SqlNode.apply():遍历生成SQL

SqlNode 接口只有一个方法:

public interface SqlNode {
    boolean apply(DynamicContext context);
}

1 2 3 每个 SqlNode 的 apply 方法负责将自己的 SQL 片段追加到 DynamicContext 中。运行时,MixedSqlNode 递归调用所有子节点的 apply 方法:

// MixedSqlNode
public class MixedSqlNode implements SqlNode {
    private final ListSqlNode> contents;

    @Override
    public boolean apply(DynamicContext context) {
        for (SqlNode sqlNode : contents) {
            sqlNode.apply(context);
        }
        return true;
    }
}

1 2 3 4 5 6 7 8 9 10 11 12 IfSqlNode**:

public class IfSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String test;
    private final SqlNode contents;

    @Override
    public boolean apply(DynamicContext context) {
        // 使用 OGNL 求值 test 表达式
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context);  // 条件为 true,拼接子节点
            return true;
        }
        return false;
    }
}

12 3 4 5 6 7 8 9 10 11 12 13 14 15 WhereSqlNode

public class WhereSqlNode extends TrimSqlNode {

    private static ListString> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n",
        "AND\r", "OR\r", "AND\t", "OR\t");

    public WhereSqlNode(Configuration configuration, SqlNode contents) {
        // where 标签就是 trim 的特例!
        // prefix = "WHERE",prefixOverrides = "AND |OR "
        super(configuration, contents, "WHERE", prefixList, null, null);
    }
}

12 3 4 5 6 7 8 9 10 11 SetSqlNode

public class SetSqlNode extends TrimSqlNode {

    public SetSqlNode(Configuration configuration, SqlNode contents) {
        // set 标签也是 trim 的特例!
        // prefix = "SET",suffixOverrides = ","
        super(configuration, contents, "SET", null, null, Arrays.asList(","));
    }
}

12 3 4 5 6 7 8 关键发现:where 和 set 都是 TrimSqlNode 的子类!where 就是 prefix="WHERE" prefixOverrides="AND |OR " 的 trim,set 就是 prefix="SET" suffixOverrides="," 的 trim。

TrimSqlNode

public class TrimSqlNode implements SqlNode {
    private final SqlNode contents;
    private final String prefix;
    private final String suffix;
    private final ListString> prefixesToOverride;
    private final ListString> suffixesToOverride;

    @Override
    public boolean apply(DynamicContext context) {
       FilteredDynamicContext filteredDynamicContext =
            new FilteredDynamicContext(context, prefix, suffix,
                prefixesToOverride, suffixesToOverride);
        // 先让子节点拼接 SQL
        contents.apply(filteredDynamicContext);
        // 再处理前缀后缀裁剪
        filteredDynamicContext.applyAll();
        return true;
    }

    private class FilteredDynamicContext extends DynamicContext {
        @Override
        public void applyAll() {
            // 1. 子节点拼接完成后,获取完整 SQL
            String sql = delegate.getSqlBuilder().toString().trim();
            // 2. 去除前缀(AND/OR)
            if (prefixesToOverride != null) {
                for (String toRemove : prefixesToOverride) {
                    if (sql.toUpperCase().startsWith(toRemove.toUpperCase())) {
                        sql = sql.substring(toRemove.length());
                        break;
                    }
                }
            }
            // 3. 去除后缀(逗号)
            if (suffixesToOverride != null) {
                for (String toRemove : suffixesToOverride) {
                    if (sql.toUpperCase().endsWith(toRemove.toUpperCase())) {
                        sql = sql.substring(0, sql.length() - toRemove.length());
                        break;
                    }
                }
            }
            // 4. 内容非空时添加 prefix/suffix
            if (sql.length() > 0) {
                if (prefix != null) sql = prefix + sql;
                if (suffix != null) sql = sql + suffix;
            }
            delegate.appendSql(sql);
        }
    }
}

12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 ForEachSqlNode

public class ForEachSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String collectionExpression;
    private final String item;
    private final String index;
    private final String open;
    private final String close;
    private final String separator;
    private final SqlNode contents;

    @Override
    public boolean apply(DynamicContext context) {
        // 1. 通过 OGNL 获取集合对象
        Iterable?> iterable = evaluator.evaluateIterable(
            collectionExpression, context.getBindings());
        int i = 0;
        for (Object o : iterable) {
            // 2. 保存旧变量(item/index 可能与外部变量同名)
            int uniqueNumber = context.getUniqueNumber();
            // 3. 将当前迭代项放入上下文
            if (o instanceof Map.Entry) {
                Map.EntryObject, Object> mapEntry = (Map.EntryObject, Object>) o;
                context.bind(item, mapEntry.getValue());
                context.bind(index, mapEntry.getKey());
            } else {
                context.bind(index, i);
                context.bind(item, o);
            }
            // 4. 添加 open/separator/close
            if (i == 0 && open != null) {
                context.appendSql(open);
            } else if (separator != null) {
                context.appendSql(separator);
            }
            // 5. 拼接每次迭代的 SQL
            contents.apply(context);
            i++;
        }
        if (i > 0 && close != null) {
            context.appendSql(close);
        }
        return true;
    }
}

1**2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

5. DynamicSqlSource:动态SQL的执行入口

// org.apache.ibatis.scripting.xmltags.DynamicSqlSource
public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 1. 创建动态上下文
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // 2. 遍历 SqlNode 树,拼接 SQL
        rootSqlNode.apply(context);
        // 3. 处理 #{ } → 替换为 ?
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        SqlSource sqlSource = sqlSourceParser.parse(
            context.getSql(), parameterType, context.getBindings());
        // 4. 生成 BoundSql
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        // 5. 绑定附加参数
        for (Map.EntryString, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }
}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 完整链路**:

DynamicSqlSource.getBoundSql()
  → DynamicContext 创建
  → rootSqlNode.apply(context)    ← 递归遍历 SqlNode 树
    → IfSqlNode: OGNL 求值 → 条件拼接
    → WhereSqlNode: 子节点拼接 → 去前缀 AND/OR → 加 WHERE
    → ForEachSqlNode: 集合遍历 → 生成 IN 列表
    → ... 其他 SqlNode
  → SqlSourceBuilder.parse()      ← `#{}` 替换为 ?
  → BoundSql 返回

1**2 3 4 5 6 7 8 9

六、常见陷阱

1. where标签不处理条件都为空的情况


select id="findUsers" 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 2 3 4 5 6 7 8 9 10 11 12 13 建议**:业务层做校验,至少传一个条件,或在 SQL 中加上恒真条件兜底。

2. set标签内忘记加逗号


set>
    if test="name != null">
        name = #{name}
    if>
    if test="age != null">
        age = #{age}
    if>
set>

set>
    if test="name != null">
        name = #{name},
    if>
    if test="age != null">
        age = #{age},
    if>
set>

1**2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

3. foreach的collection属性写错

// 接口方法
ListUser> findByIds(ListLong> ids);       // collection="list"
ListUser> findByIds(@Param("ids") ListLong> ids);  // collection="ids"
ListUser> findByIds(Long[] ids);            // collection="array"

1 2 3 4 不使用 @Param 时,List 默认用 list,数组默认用 array。传错会报 There is no getter for property named 'xxx'

4. OGNL中字符串比较用单引号


if test="type == "admin"">

if test="type == 'admin'">

if test='type == "admin".toString()'>

1 2 3 4 5 6 7 8

5. foreach批量插入量过大


insert id="batchInsert">
    INSERT INTO user (name, age) VALUES
    foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age})
    foreach>
insert>

1 2 3 4 5 6 7 建议**:分批执行,每批 500-1000 条;或使用 MyBatis 的 ExecutorType.BATCH 模式。

七、完整示例:动态查询

mapper namespace="com.example.mapper.UserMapper">


    sql id="userColumns">
        id, name, age, email, status, create_time
    sql>


    select id="findUsers" resultType="User">
        SELECT include refid="userColumns"/>
        FROM user
        where>
            if test="status != null">
                AND status = #{status}
            if>
            if test="name != null and name != ''">
                bind name="namePattern" value="'%' + name + '%'" />
                AND name LIKE #{namePattern}
            if>
            if test="ids != null and ids.size() > 0">
                AND id IN
                foreach collection="ids" item="id" open="(" separator="," close=")">
                    #{id}
                foreach>
            if>
        where>
        choose>
            when test="sortField != null and sortField != ''">
                ORDER BY ${sortField}
                if test="sortOrder != null">
                    ${sortOrder}
                if>
            when>
            otherwise>
                ORDER BY create_time DESC
            otherwise>
        choose>
    select>


    update id="updateUser" parameterType="User">
        UPDATE user
        set>
            if test="name != null">name = #{name},if>
            if test="age != null">age = #{age},if>
            if test="email != null">email = #{email},if>
            if test="status != null">status = #{status},if>
        set>
        WHERE id = #{id}
    update>

mapper>

12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 注意上面 ORDER BY 使用了 ${},因为排序字段名是标识符不能用 #{},必须在 Java 层做白名单校验

八、常见面试追问

Q1:trim标签和where/set标签是什么关系?

where 和 set 都是 trim 的特例。where 等价于 prefix="WHERE" prefixOverrides="AND |OR ",set 等价于 prefix="SET" suffixOverrides=","。在源码中,WhereSqlNode 和 SetSqlNode 都继承自 TrimSqlNode,构造时传入对应的参数即可。

Q2:动态SQL会影响性能吗?

会有一点影响,但通常可以忽略。动态 SQL 每次执行时都需要遍历 SqlNode 树、OGNL 求值、拼接 SQL 字符串,这个过程有开销。但 OGNL 表达式编译有缓存,SqlNode 树在启动时就已经构建好,运行时只是遍历和求值。相比数据库 I/O,这点开销微不足道。

需要注意的是,动态 SQL 会导致 SQL 文本变化,比如不同条件组合生成不同的 SQL,这可能影响数据库的 SQL 缓存命中率。但这也是无法避免的——不同的查询条件本来就应该生成不同的 SQL。

Q3:SQL注入和动态SQL有关系吗?

有间接关系。动态 SQL 中的 ${} 拼接和 if/foreach 标签本身不会导致注入,但如果在动态 SQL 中使用了 ${}(如 ORDER BY 场景),且未做白名单校验,就存在注入风险。反过来,动态 SQL 的 #{} + foreach 等标签,恰恰是防止注入的正确写法——用 #{} + foreach 替代 ${} 拼接 IN 列表。

Q4:动态SQL和静态SQL的区别是什么?

静态 SQL(RawSqlSource):SQL 在启动时就能确定完整内容,不含任何动态标签和 ${},只包含 #{} 占位符。解析一次,执行时直接拿来用。

动态 SQL(DynamicSqlSource):SQL 在执行时才能确定内容,包含 if/where/foreach 等标签。每次执行都需要遍历 SqlNode 树、OGNL 求值,动态拼接 SQL。

判断依据:XMLScriptBuilder 解析时,如果发现任何动态标签或 ${},就标记 isDynamic=true,使用 DynamicSqlSource。

Q5:为什么OGNL表达式判断字符串相等要用单引号?

OGNL 中双引号用于界定字符串字面量,而 MyBatis XML 的属性值本身用双引号包裹。所以 test 属性内的双引号会和 XML 属性的双引号冲突,必须用单引号表示字符串值。

九、总结

记住三个核心要点:

1. 9个标签各有分工:
   if 条件判断、choose 多分支、where 智能WHERE、set 智能SET、
   trim 自定义裁剪、foreach 集合遍历、bind 变量绑定、
   sql/include 片段复用
   where = trim(prefix="WHERE", prefixOverrides="AND |OR ")
   set = trim(prefix="SET", suffixOverrides=",")

2. 解析原理 = SqlNode 树 + OGNL 求值
   XMLScriptBuilder.parseDynamicTags() 将 XML 标签解析为 SqlNode 树
   运行时 DynamicSqlSource 遍历 SqlNode 树
   每个节点的 apply() 方法向 DynamicContext 追加 SQL
   OGNL 表达式编译结果有缓存,不会重复编译

3. 注意常见陷阱:
   where 条件全空 → 无 WHERE 子句 → 全表查询
   set 内每个条件后必须加逗号
   foreach 的 collection 属性要和参数对应
   OGNL 字符串比较用单引号
   批量操作要控制每批数量

12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 面试回答模板

MyBatis 动态 SQL 有 9 个标签:if 做条件判断,choose/when/otherwise 做多分支选择,where 自动去除前缀 AND/OR,set 自动去除末尾逗号,trim 是自定义裁剪(where 和 set 底层都继承自 TrimSqlNode),foreach 遍历集合做 IN 查询或批量操作,bind 创建变量,sql/include 做 SQL 片段复用。 解析原理上,MyBatis 在启动时通过 XMLScriptBuilder.parseDynamicTags() 将 XML 中的动态标签递归解析为一棵 SqlNode 树,每个标签对应一个 SqlNode 实现类。运行时,DynamicSqlSource.getBoundSql() 遍历这棵树,每个 SqlNode 的 apply() 方法向 DynamicContext 追加 SQL 片段。if 标签通过 OGNL 表达式求值决定是否拼接,where/set 通过 TrimSqlNode 的裁剪逻辑处理前缀后缀,foreach 通过迭代集合拼接多次 SQL。OGNL 表达式编译有缓存,不会重复解析。 使用上需要注意几个常见陷阱:where 条件全空会导致全表查询,set 内每个条件后必须加逗号,foreach 的 collection 属性要和参数类型对应,批量操作要控制每批数量避免超限。

第36篇-动态SQL与OGNL
http://www.clxhxhhr.top/posts/981/
作者
clxstart
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。