在实际业务开发中,数据库查询条件几乎不会一成不变:前端多条件筛选、非必填参数、动态排序、分页拼接、批量增删改等场景,固定写死的SQL语句完全无法适配业务需求。
MyBatis 动态SQL是其核心核心能力之一,区别于静态SQL,可根据参数是否为空、参数值、业务状态自动拼接、裁剪、优化SQL语句,彻底解决多条件动态查询、批量操作、条件分支适配问题。
本文将从核心原理、全套标签详解、场景Demo、对比表格、实战规范、常见坑点全方位梳理,覆盖99%企业开发动态SQL场景。
一、动态SQL核心原理与优势
1.1 核心原理
MyBatis 在执行SQL前,会通过OGNL表达式解析XML中的动态标签,根据传入的参数对象属性值,动态判断是否拼接SQL片段,最终生成一条合规、无语法错误的可执行SQL,再交由数据库执行。
1.2 动态SQL vs 静态SQL 对比
对比维度 | 静态SQL | 动态SQL |
|---|---|---|
写法特点 | SQL语句固定写死,无逻辑判断 | 通过标签+表达式动态拼接SQL片段 |
参数适配性 | 参数缺失会报SQL语法错误、查询异常 | 自动忽略空参数,适配非必填条件 |
业务场景 | 仅适用于固定条件查询、简单CRUD | 多条件筛选、批量操作、动态排序/分页 |
维护成本 | 多场景需写多条SQL,冗余极高 | 一条SQL适配全场景,统一维护 |
语法安全性 | 易出现多余and/or、逗号等语法问题 | 内置语法裁剪机制,杜绝语法错误 |
1.3 动态SQL全套核心标签总览
标签 | 核心作用 | 适用场景 |
|---|---|---|
if | 单条件判断,满足条件则拼接SQL | 非必填查询条件、参数动态拼接 |
where | 自动去除多余的 and/or,智能拼接where关键字 | 多if条件组合查询,解决语法冗余问题 |
trim | 自定义前后缀、去除指定冗余字符,高度灵活 | where/set无法满足的特殊裁剪场景 |
set | 动态拼接update字段,自动去除末尾多余逗号 | 动态更新字段(部分字段更新) |
choose/when/otherwise | 多条件互斥分支,只执行一个匹配条件 | 单选条件筛选(优先级判断) |
foreach | 遍历集合、数组,批量拼接SQL片段 | 批量查询、批量新增、批量删除 |
bind | 绑定自定义变量,简化表达式、防止SQL注入 | 模糊查询、复杂参数处理 |
include/sql | 抽取通用SQL片段,复用代码 | 公共字段、通用查询条件复用 |
二、全标签深度解析 + 可直接运行Demo
统一前置实体与参数:本文所有Demo基于User 实体类和user 数据表,适配SpringBoot + MyBatis 常规项目。
// User实体类 public class User { private Long id; private String username; private Integer age; private String phone; private Integer status; // 状态 0-禁用 1-正常 private LocalDateTime createTime; // getter/setter 省略 } // 前端查询参数DTO(动态查询入参) public class UserQuery { private String username; // 模糊查询 private Integer age; // 精准查询 private Integer status; // 状态筛选 private List<Long> idList; // 批量ID // getter/setter 省略 }2.1 if 标签:基础单条件动态拼接
作用:判断参数是否非空,满足条件则拼接对应SQL,最基础、使用频率最高的动态标签。
语法规则:test属性为OGNL表达式,支持非空判断、数值判断、字符串判断。
<!-- 多条件动态查询用户 --> <select id="listUserByCondition" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> SELECT id, username, age, phone, status, create_time FROM user WHERE del_flag = 0 <!-- 用户名非空则模糊查询 --> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <!-- 年龄不为空则精准查询 --> <if test="age != null"> AND age = #{age} </if> <!-- 状态不为空则筛选 --> <if test="status != null"> AND status = #{status} </if> </select>坑点注意:单纯使用if标签,首个条件为空时,会残留多余的AND,导致SQL语法报错,需配合where标签使用。
2.2 where 标签:智能处理查询条件前缀
核心优势:1. 自动识别并添加where关键字;2. 自动去除第一个条件前多余的 and/or;3. 无任何条件时,不生成where语句。
完美解决if标签单独使用的语法报错问题,企业开发查询场景必用。
<select id="listUserByCondition" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> SELECT id, username, age, phone, status, create_time FROM user <where> del_flag = 0 <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="age != null"> AND age = #{age} </if> <if test="status != null"> AND status = #{status} </if> </where> </select>2.3 trim 标签:自定义语法裁剪(万能替代where/set)
作用:自定义拼接前缀、后缀,同时裁剪指定的首尾多余字符,是动态SQL的万能语法修复标签。
核心属性:
prefix:整体拼接的前缀字符串
suffix:整体拼接的后缀字符串
prefixOverrides:需要去除的首部字符(and/or)
suffixOverrides:需要去除的尾部字符(逗号)
Demo:trim实现where标签效果
<select id="listUserByTrim" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> SELECT id, username, age, phone, status, create_time FROM user <trim prefix="WHERE" prefixOverrides="AND|OR"> del_flag = 0 <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> </trim> </select>2.4 set 标签:动态更新字段专用
场景:后台修改用户信息时,只更新传入的字段,空字段不更新,避免覆盖原有数据。
核心优势:自动去除最后一个字段后的多余逗号,彻底解决动态更新的语法报错问题。
<update id="updateUserDynamic" parameterType="com.xxx.entity.User"> UPDATE user <set> <if test="username != null and username != ''"> username = #{username}, </if> <if test="age != null"> age = #{age}, </if> <if test="phone != null and phone != ''"> phone = #{phone}, </if> <if test="status != null"> status = #{status} </if> </set> WHERE id = #{id} AND del_flag = 0 </update>2.5 choose/when/otherwise:互斥分支判断
核心特点:多条件单选互斥,自上而下匹配,匹配成功一个分支后,不再执行后续分支,类似 Java 的if-else if-else。
适用场景:优先级筛选、唯一条件匹配(如:优先ID查询,无ID则用户名查询,最后查全部)
<select id="getUserByPriority" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> SELECT id, username, age, status FROM user <where> del_flag = 0 <choose> <!-- 优先根据ID精准查询 --> <when test="id != null"> AND id = #{id} </when> <!-- ID为空则根据用户名查询 --> <when test="username != null and username != ''"> AND username = #{username} </when> <!-- 所有条件为空,默认查询正常状态用户 --> <otherwise> AND status = 1 </otherwise> </choose> </where> </select>2.6 foreach 标签:批量操作核心(高频必考)
核心场景:批量删除、批量新增、IN集合查询、批量更新。
核心属性说明:
属性 | 作用 |
|---|---|
collection | 遍历集合/数组参数名(List填list、数组填array、自定义参数填参数名) |
item | 遍历后的单个元素别名 |
index | 遍历下标/Map键名 |
open | 遍历整体前缀(如 IN ( 的左括号) |
close | 遍历整体后缀(如 ) 的右括号) |
separator | 元素之间的分隔符(逗号、OR等) |
Demo1:IN 批量查询(根据ID集合查询用户)
<select id="listUserByIdList" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> SELECT * FROM user WHERE del_flag = 0 <if test="idList != null and idList.size() > 0"> AND id IN <foreach collection="idList" item="id" open="(" close=")" separator=","> #{id} </foreach> </if> </select>Demo2:批量删除用户
<delete id="batchDeleteUser" parameterType="java.util.List"> DELETE FROM user WHERE id IN <foreach collection="list" item="id" open="(" close=")" separator=","> #{id} </foreach> </delete>Demo3:批量新增用户(高性能)
<insert id="batchInsertUser" parameterType="java.util.List"> INSERT INTO user (username, age, phone, status, create_time) VALUES <foreach collection="list" item="user" separator=","> (#{user.username}, #{user.age}, #{user.phone}, #{user.status}, NOW()) </foreach> </insert>2.7 bind 标签:参数绑定与模糊查询优化
作用:自定义绑定变量,简化OGNL表达式,解决数据库模糊查询兼容性问题(MySQL、Oracle适配),同时预防SQL注入。
<select id="listUserByLike" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> <!-- 绑定模糊查询变量,全局复用 --> <bind name="likeUsername" value="'%' + username + '%'"/> SELECT * FROM user <where> del_flag = 0 <if test="username != null and username != ''"> AND username LIKE #{likeUsername} </if> </where> </select>2.8 sql + include:SQL片段复用
作用:抽取通用SQL片段(公共字段、通用查询条件),避免代码冗余,统一维护。
<!-- 抽取公共查询字段 --> <sql id="userCommonField"> id, username, age, phone, status, create_time </sql> <!-- 抽取通用删除条件 --> <sql id="delFlagCondition"> del_flag = 0 </sql> <select id="listAllUser" resultType="com.xxx.entity.User"> SELECT <include refid="userCommonField"/> FROM user <where> <include refid="delFlagCondition"/> </where> </select>三、动态SQL高频场景整合Demo
3.1 综合多条件筛选(if+where+bind)
适配:用户名模糊、年龄区间、状态筛选,全非必填参数
<select id="listUserComplexQuery" resultType="com.xxx.entity.User" parameterType="com.xxx.dto.UserQuery"> <bind name="likeName" value="'%' + username + '%'"/> SELECT id, username, age, status, create_time FROM user <where> del_flag = 0 <if test="username != null and username != ''"> AND username LIKE #{likeName} </if> <if test="minAge != null"> AND age >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{maxAge} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>3.2 动态字段更新(set+if)
只更新传入的非空字段,保留数据库原有旧数据
<update id="updateUserSelective" parameterType="com.xxx.entity.User"> UPDATE user <set> <if test="username != null and username != ''">username=#{username},</if> <if test="age != null">age=#{age},</if> <if test="phone != null and phone != ''">phone=#{phone},</if> <if test="status != null">status=#{status},</if> update_time = NOW() </set> WHERE id = #{id} AND del_flag = 0 </update>四、动态SQL核心避坑指南(高频报错)
常见坑点 | 报错原因 | 解决方案 |
|---|---|---|
多余AND/OR语法错误 | 首个if条件为空,残留前置AND | 所有多条件查询统一使用 <where> 标签 |
UPDATE末尾多余逗号 | 动态字段最后一条拼接逗号 | 更新语句统一使用 <set> 标签 |
foreach空集合报错 | 集合为空时生成 IN() 空语法 | 遍历前加判断:size>0 |
字符串空串判断遗漏 | 只判断null,未判断空字符串,导致无效查询 | 字符串统一判断: |
choose多分支同时生效 | 误用if替代choose,未理解互斥逻辑 | 单选条件必须使用choose,不可用多个if |
五、企业开发动态SQL规范总结
查询场景:多条件非必填查询,固定搭配
where + if,杜绝手写where关键字更新场景:局部动态更新,必须使用
set + if,防止逗号语法错误单选分支:优先级、互斥条件,强制使用
choose/when/otherwise批量操作:所有集合遍历必须用
foreach,且前置非空判断代码复用:公共字段、通用条件统一抽取
sql片段,全局include引用参数判断:数值型只判null,字符串必须同时判null和空串
六、全文总结
MyBatis动态SQL的核心价值是适配业务不确定性,通过8大核心标签,可完美解决:多条件筛选、局部更新、批量操作、分支查询等所有复杂业务场景。
开发核心口诀:查询用where、更新用set、单选用choose、批量用foreach、复用抽sql、空参必判断。
掌握本文所有Demo与避坑要点,可完全覆盖企业开发中99%的动态SQL开发场景,杜绝语法报错与代码冗余问题。