一文详解MyBatis中动态SQL的封装原理与常用标签实战应用
作者:身如柳絮随风扬
告别繁琐的SQL拼接,让动态查询像搭积木一样简单
一、为什么需要动态SQL?
在实际开发中,我们经常会遇到需要根据不同的条件拼接SQL语句的场景。传统的做法是使用Java代码进行字符串拼接,但这种方式不仅代码冗余、易出错,还存在SQL注入的风险。MyBatis提供的动态SQL机制,允许我们在XML映射文件中使用标签来构建灵活的SQL语句,让代码更清晰、更安全。
核心优势:
- 无需手动处理多余的空格、AND/OR、逗号分隔符
- 基于OGNL表达式,与Java对象无缝集成
- 提升可维护性,SQL结构一目了然
二、动态SQL的执行流程
MyBatis在解析动态SQL时,会根据传入的参数动态生成最终的SQL语句。下图展示了从XML到预编译SQL的完整过程:

三、常用动态SQL标签详解
1.<if>– 条件判断
最简单的条件标签,当条件满足时拼接SQL片段。
语法:
<if test="条件表达式">
SQL片段
</if>示例: 根据姓名和年龄动态查询用户
<select id="findUsers" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null and age > 0">
AND age = #{age}
</if>
</select>注意: WHERE 1=1 是为了防止所有条件都不满足时出现语法错误。更好的替代方案是使用 <where> 标签。
2.<where>– 智能处理查询条件
自动处理AND/OR关键字,并去除多余的连接词。
示例: 重构上面的查询
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null and age > 0">
AND age = #{age}
</if>
</where>
</select>当 name 有值而 age 为 null 时,生成的SQL为:
SELECT * FROM user WHERE name = ?<where>标签会自动去掉开头的AND。
3.<set>– 动态更新字段
在 <update> 语句中智能处理逗号分隔符。
示例: 动态更新用户信息
<update id="updateUser">
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>
<set> 会自动去掉末尾多余的逗号,并保证至少有一个字段被更新。
4.<foreach>– 遍历集合/数组
常用于 IN 查询、批量插入、批量删除。
属性说明:
| 属性 | 作用 |
|---|---|
collection | 要遍历的集合/数组名称 |
item | 每次迭代的当前元素别名 |
index | 当前元素的索引(或map的key) |
open | 遍历开始拼接的字符串 |
close | 遍历结束拼接的字符串 |
separator | 元素之间的分隔符 |
示例1: 批量查询(IN条件)
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
示例2: 批量插入
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
常见坑点: collection 默认支持 list、array、map。如果是自定义参数名,需要使用 @Param 注解指定。
5.<trim>– 自定义前缀/后缀处理
<where> 和 <set> 本质上都是 <trim> 的快捷方式,当需要更精细的控制时可直接使用 <trim>。
属性:
prefix– 整体前添加的字符串suffix– 整体后添加的字符串prefixOverrides– 去除前缀中的指定字符串suffixOverrides– 去除后缀中的指定字符串
等价关系:
<!-- <where> 等价于 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- <set> 等价于 -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
自定义示例: 动态添加 ORDER BY 子句
<select id="selectWithOrder" resultType="User">
SELECT * FROM user
<trim prefix="ORDER BY" suffixOverrides=",">
<if test="orderByName">name,</if>
<if test="orderByAge">age,</if>
</trim>
</select>
6.<choose> / <when> / <otherwise>– 分支选择
类似Java的 switch-case,只执行第一个满足条件的分支。

示例: 按优先级排序查询(姓名优先,否则年龄,否则默认)
<select id="findUsersWithPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="age != null">
AND age = #{age}
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
四、综合实战:员工管理系统动态查询
下面是一个结合多个标签的完整示例:
<mapper namespace="com.example.EmployeeMapper">
<!-- 动态查询员工 -->
<select id="searchEmployees" resultType="Employee">
SELECT * FROM employee
<where>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
<if test="minSalary != null">
AND salary >= #{minSalary}
</if>
<if test="maxSalary != null">
AND salary <= #{maxSalary}
</if>
<if test="keywords != null and keywords != ''">
AND (name LIKE CONCAT('%', #MyBatis动态SQL封装,MyBatis动态SQL使用,MyBatis动态SQL,MyBatis SQL, '%')
OR position LIKE CONCAT('%', #MyBatis动态SQL封装,MyBatis动态SQL使用,MyBatis动态SQL,MyBatis SQL, '%'))
</if>
<choose>
<when test="hireDateStart != null and hireDateEnd != null">
AND hire_date BETWEEN #{hireDateStart} AND #{hireDateEnd}
</when>
<when test="hireDateStart != null">
AND hire_date >= #{hireDateStart}
</when>
<when test="hireDateEnd != null">
AND hire_date <= #{hireDateEnd}
</when>
</choose>
</where>
<if test="orderBy != null">
<trim prefix="ORDER BY" suffixOverrides=",">
<if test="orderBy.name">name,</if>
<if test="orderBy.salary">salary,</if>
<if test="orderBy.hireDate">hire_date,</if>
</trim>
</if>
</select>
</mapper>
五、最佳实践与常见问题
推荐做法
- 优先使用
<where>/<set>而非手写1=1或SET后跟逗号。 - 结合
<trim>处理复杂的动态前缀/后缀逻辑。 - 使用
@Param为集合参数命名,避免依赖默认的list/array。 - 对字符串判空:
test="name != null and name != ''"。
常见错误
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| 动态SQL不生效 | 忘记在<select>中嵌入动态标签 | 检查标签是否正确嵌套 |
| 多余的AND/OR | 条件未全部满足时前缀残留 | 使用<where>或<trim prefixOverrides> |
| 批量插入性能差 | foreach生成超大SQL | 改用分批处理或JDBC batch |
| OGNL表达式异常 | 使用了Java的&&/` |
六、总结
MyBatis的动态SQL通过一套简洁的XML标签,完美解决了传统JDBC拼接SQL的痛点。掌握以下核心标签即可应对90%的动态场景:
| 标签 | 核心用途 |
|---|---|
<if> | 简单条件判断 |
<where> | 智能查询条件组装 |
<set> | 智能更新字段组装 |
<foreach> | 遍历集合(IN、批量操作) |
<trim> | 自定义前缀/后缀处理 |
<choose> | 多分支互斥选择 |
动态SQL的本质是在XML层面基于OGNL表达式进行零逻辑的SQL片段组合。理解这一思想后,你可以灵活组合这些标签,写出既优雅又安全的动态SQL。
延伸思考: 如果动态条件非常复杂(超过5个分支),建议考虑将部分逻辑迁移到Java层,使用@SelectProvider注解或构建查询对象模式,以保持XML的可读性。
到此这篇关于一文详解MyBatis中动态SQL的封装原理与常用标签实战应用的文章就介绍到这了,更多相关MyBatis动态SQL应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
