一、为什么需要动态 SQL在实际开发中我们几乎不可能写出一成不变的 SQL。最典型的场景就是多条件组合查询——用户可能只填了姓名也可能同时填了姓名、类型和状态还可能什么都不填。如果用传统 JDBC 拼接字符串你会写出这样的代码StringBuilder sql new StringBuilder(select * from employee where 11); if (name ! null) { sql.append( and name like %).append(name).append(%); } if (status ! null) { sql.append( and status ).append(status); } // ... 一堆 if-else容易拼错、容易 SQL 注入痛点很明显拼接易出错少一个空格、多一个逗号SQL 就废了SQL 注入风险手动拼字符串无法使用预编译参数可读性差业务逻辑和 SQL 拼接混在一起代码像面条MyBatis 的动态 SQL 机制就是为了优雅地解决这些问题而诞生的。二、动态 SQL 的本质MyBatis 动态 SQL 的底层原理是OGNL 表达式 XML 标签解析。MyBatis 在解析 Mapper XML 时会将这些标签转换为SqlNode对象树运行时根据传入参数的值动态地裁剪和拼接 SQL 片段最终生成一条完整的、可执行的预编译 SQL。简单理解动态 SQL 在 XML 里写 if/elseMyBatis 帮你在运行时智能拼 SQL。三、九大核心标签全解析3.1if—— 条件判断最基础的守门员if是使用频率最高的动态 SQL 标签用于根据参数值决定是否拼接某段 SQL。select idpageQuery resultTypeEmployee select * from employee where if testname ! null and name ! and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if /where order by create_time desc /select要点test属性使用 OGNL 表达式支持!、、and、or等逻辑运算字符串类型建议同时判断! null和! 否则空字符串会拼出无效条件if体内的 SQL 片段可以包含and/or前缀配合where使用可以自动去除3.2where—— 智能处理 WHERE 子句where标签会做两件聪明的事当内部有条件满足时自动添加WHERE关键字自动去除条件开头多余的AND或OR!-- 如果 name 和 status 都为 null生成select * from employee -- !-- 如果只有 name 不为 null生成select * from employee WHERE name like ... -- !-- 不会出现 WHERE and name like ... 的尴尬 -- select idlist resultTypeEmployee select * from employee where if testname ! null and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if /where /select注意where只会去除开头的多余 AND/OR不会处理中间和结尾的。所以if里的条件统一以and开头是最佳实践。3.3set—— 更新操作的好搭档更新操作最头疼的问题是用户只改了名字你总不能把其他字段都更新为 null 吧set标签专门解决这个问题。update idupdate parameterTypeEmployee update employee set if testname ! nullname #{name},/if if testusername ! nullusername #{username},/if if testphone ! nullphone #{phone},/if if testsex ! nullsex #{sex},/if if teststatus ! nullstatus #{status},/if /set where id #{id} /updateset标签的作用自动在内容前添加SET关键字自动去除最后一个多余的逗号,这就是为什么每个if里末尾都写了逗号但不用担心 SQL 报错。3.4choose/when/otherwise—— 多选一的条件分支if是满足就拼但有时候你需要的是只选一个的逻辑类似 Java 的switch-case。select idlistByPriority resultTypeEmployee select * from employee where choose when testid ! null id #{id} /when when testname ! null and name ! name like concat(%, #{name}, %) /when otherwise status 1 /otherwise /choose /where /select执行逻辑按顺序判断when一旦有满足的就使用后面的不再判断如果所有when都不满足执行otherwiseotherwise可以省略适用场景优先级查询、互斥条件选择。3.5foreach—— 批量操作之王当你需要处理IN查询或批量插入时foreach是不可或缺的。IN 查询select idgetByIds resultTypeCategory select * from category where id in foreach collectionids itemid open( separator, close) #{id} /foreach /select生成效果select * from category where id in (1, 2, 3)批量插入insert idinsertBatch insert into dish_flavor (dish_id, name, value) values foreach collectionflavors itemflavor separator, (#{flavor.dishId}, #{flavor.name}, #{flavor.value}) /foreach /insert生成效果insert into dish_flavor (dish_id, name, value) values (1,辣度,微辣), (1,甜度,半糖)属性说明属性说明collection集合参数名对应接口方法的Param值或参数属性名item迭代变量名在#{}中引用index索引变量名List 为下标Map 为 keyopen整个循环体前添加的字符串close整个循环体后添加的字符串separator每次迭代之间的分隔符⚠️ 注意批量插入时MySQL 对 SQL 长度有限制max_allowed_packet默认 4MB数据量特别大时建议分批插入。3.6trim—— 万能裁剪器where和set本质上都是trim的语法糖!-- where 等价于 -- trim prefixWHERE prefixOverridesAND |OR ... /trim !-- set 等价于 -- trim prefixSET suffixOverrides, ... /trimtrim的四个属性属性说明prefix给内容整体添加的前缀suffix给内容整体添加的后缀prefixOverrides去除内容开头指定的字符串suffixOverrides去除内容末尾指定的字符串自定义 trim 示例select idqueryByCondition resultTypeEmployee select * from employee trim prefixWHERE prefixOverridesAND |OR if testname ! null and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if /trim /select当只需要where和set的能力时直接用它们更简洁遇到更复杂的裁剪需求时trim才上场。3.7sql/include—— SQL 片段复用当你发现多个查询中重复出现相同的字段列表或条件片段时可以用sql提取公共部分用include引用。!-- 定义公共字段 -- sql idemployeeColumns id, name, username, phone, sex, id_number, status, create_time, update_time /sql !-- 定义公共查询条件 -- sql idemployeeCondition if testname ! null and name ! and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if /sql !-- 引用 -- select idpageQuery resultTypeEmployee select include refidemployeeColumns/ from employee where include refidemployeeCondition/ /where order by create_time desc /select select idlist resultTypeEmployee select include refidemployeeColumns/ from employee where include refidemployeeCondition/ /where /select3.8bind—— 变量绑定bind允许你在 SQL 中创建一个变量对 OGNL 表达式的结果进行预处理。select idfuzzyQuery resultTypeEmployee bind namepattern value% name % / select * from employee where name like #{pattern} /select典型应用场景模糊查询的通配符拼接避免在每个数据库方言中写不同的 concat对参数做预处理如大小写转换、字符串截取等!-- 大小写不敏感查询 -- select idsearch resultTypeEmployee bind namelowerName valuename.toLowerCase() / select * from employee where LOWER(name) like CONCAT(%, #{lowerName}, %) /select四、动态 SQL 的性能考量4.1 SQL 缓存与动态 SQLMyBatis 有一个重要的内部机制每个查询都会生成一个 MappedStatement其中包含 SQL 的解析模板。动态 SQL 的条件分支不会在启动时就固定而是在每次执行时根据参数重新解析。但这并不意味着严重的性能问题因为 MyBatis 内部对 SQL 解析做了缓存优化——相同参数组合生成的 SQL 会被缓存不会每次都重新解析 OGNL。4.2 批量操作的选择方案优点缺点foreach拼接一次 SQL 完成SQL 过长可能超限Batch Executor安全无 SQL 长度限制多次网络交互分批 foreach兼顾效率和安全需要手动分批推荐数据量小 500 条用foreach数据量大用 MyBatis 的BatchExecutor或分批插入。五、一张图总结动态 SQL 标签速查 ├── 条件判断 │ ├── if → 满足条件就拼接 │ └── choose → 多选一when/otherwise ├── 子句修饰 │ ├── where → 自动加 WHERE去开头 AND/OR │ ├── set → 自动加 SET去末尾逗号 │ └── trim → 自定义前后缀裁剪where/set 的底层实现 ├── 集合迭代 │ └── foreach → IN 查询、批量插入 ├── 复用与绑定 │ ├── sql/include→ SQL 片段定义与引用 │ └── bind → OGNL 变量绑定 └── 注解方式 └── script → 注解中写动态 SQL结语动态 SQL 是 MyBatis 最核心的特性之一掌握它不仅是会用几个标签更关键的是理解它的设计哲学——将 SQL 的静态结构与运行时的动态条件解耦。从if到foreach从where到trim每个标签都有其最佳使用场景。在实际项目中灵活组合这些标签才能写出既优雅又高效的持久层代码。最后记住一句话动态 SQL 让你写更少的代码做更多的事——但前提是你真的理解每个标签的行为边界。