工作中Mybatis动态SQL的使用
一、基础概述
1、什么是动态SQL?
简单说:动态SQL就是根据条件自动拼接SQL语句。
为什么要用动态SQL?
举个例子:查询用户列表时,用户可能只输入用户名,也可能同时输入用户名和手机号,还可能什么都不输入。如果不用动态SQL,你就得写多个方法:
// 不用动态SQL,要写多个方法List<User> listAll(); // 查全部List<User> listByName(String userName); // 按用户名查List<User> listByPhone(String phone); // 按手机号查List<User> listByNameAndPhone(String userName, String phone); // 按用户名和手机号查用动态SQL,只需要一个方法:
// 用动态SQL,一个方法搞定List<User> list(UserQueryDto query);2、动态SQL能解决什么问题?
| 问题 | 动态SQL解决方案 |
|---|---|
| 条件不确定 | 根据参数有无动态拼接条件 |
| SQL拼接繁琐 | 自动处理AND、OR、逗号等 |
| 代码冗余 | 一个SQL搞定多种情况 |
二、常用动态SQL标签
1、if标签 - 条件判断
最常用的标签! 用于判断参数是否满足条件。
基本语法
<if test="判断条件"> SQL语句</if>使用示例
<select id="list" resultType="User"> SELECT * FROM t_user <where> <!-- 如果userName不为空,就拼接这个条件 --> <if test="userName != null and userName != ''"> AND user_name = #{userName} </if> <!-- 如果phone不为空,就拼接这个条件 --> <if test="phone != null and phone != ''"> AND phone = #{phone} </if> <!-- 如果status不为空,就拼接这个条件 --> <if test="status != null"> AND status = #{status} </if> </where></select>不同类型的判断方式
| 类型 | 判断方式 | 示例 |
|---|---|---|
| Integer类型 | 只判断null | |
| String类型 | 判断null和空串 | |
| List类型 | 判断null和size | |
| Boolean类型 | 直接判断 |
新人注意:Integer类型判断时不要加空字符串判断!值为0时会被误判为空。
<!-- 错误:Integer类型加了空字符串判断 --><if test="status != null and status != ''"> AND status = #{status}</if><!-- 当status=0时,这个条件不会生效! -->
<!-- 正确:Integer类型只判断null --><if test="status != null"> AND status = #{status}</if>2、where标签 - 智能处理WHERE
where标签的作用:
- 自动添加WHERE关键字
- 自动去除多余的AND或OR
使用示例
<select id="list" resultType="User"> SELECT * FROM t_user <where> <if test="userName != null and userName != ''"> AND user_name = #{userName} </if> <if test="phone != null and phone != ''"> AND phone = #{phone} </if> </where></select>效果说明:
| 情况 | 生成的SQL |
|---|---|
| 两个条件都有 | SELECT * FROM t_user WHERE user_name = ? AND phone = ? |
| 只有userName | SELECT * FROM t_user WHERE user_name = ? |
| 条件都为空 | SELECT * FROM t_user |
注意: where标签会自动去掉第一个AND,所以if里面的AND写在前面没问题。
3、set标签 - 智能处理UPDATE
set标签的作用:
- 自动添加SET关键字
- 自动去除多余的逗号
使用示例
<update id="update"> UPDATE t_user <set> <if test="userName != null and userName != ''"> user_name = #{userName}, </if> <if test="phone != null and phone != ''"> phone = #{phone}, </if> <if test="email != null and email != ''"> email = #{email}, </if> </set> WHERE id = #{id}</update>效果: 只更新传入的字段,没传的不更新。
4、choose/when/otherwise - 多选一
类似Java的switch-case,只执行第一个满足条件的分支。
使用示例
<select id="query" resultType="User"> SELECT * FROM t_user WHERE 1=1 <choose> <when test="userName != null and userName != ''"> AND user_name = #{userName} </when> <when test="phone != null and phone != ''"> AND phone = #{phone} </when> <otherwise> AND status = 1 </otherwise> </choose></select>效果说明:
- 如果userName有值,只按userName查
- 如果userName没值但phone有值,只按phone查
- 如果都没有,就查status=1的数据
5、foreach标签 - 遍历集合
用于批量操作:批量插入、批量更新、IN查询等。
基本语法
<foreach collection="集合名" item="元素名" separator="分隔符" open="开始符号" close="结束符号"> #{元素名}</foreach>| 属性 | 说明 |
|---|---|
| collection | 集合参数名 |
| item | 遍历时每个元素的变量名 |
| separator | 元素之间的分隔符 |
| open | 以什么开始 |
| close | 以什么结束 |
使用场景
(1)IN查询
<select id="listByIds" resultType="User"> SELECT * FROM t_user WHERE id IN <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach></select>生成的SQL:SELECT * FROM t_user WHERE id IN (1, 2, 3)
(2)批量插入
<insert id="insertBatch"> INSERT INTO t_user (user_name, phone, status) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.phone}, #{user.status}) </foreach></insert>(3)批量更新
<update id="updateBatch"> <foreach collection="list" item="user" separator=";"> UPDATE t_user <set> <if test="user.userName != null"> user_name = #{user.userName}, </if> <if test="user.phone != null"> phone = #{user.phone}, </if> </set> WHERE id = #{user.id} </foreach></update>6、trim标签 - 自定义处理
更灵活地处理前缀后缀。
使用示例
<!-- 等价于where标签 --><trim prefix="WHERE" prefixOverrides="AND"> <if test="userName != null and userName != ''"> AND user_name = #{userName} </if> <if test="phone != null and phone != ''"> AND phone = #{phone} </if></trim>
<!-- 等价于set标签 --><trim prefix="SET" suffixOverrides=","> <if test="userName != null and userName != ''"> user_name = #{userName}, </if> <if test="phone != null and phone != ''"> phone = #{phone}, </if></trim>三、实际工作中的使用场景
1、查询列表(最常用)
<select id="list" resultType="UserVo"> SELECT * FROM t_user <where> <if test="userName != null and userName != ''"> AND user_name LIKE CONCAT('%', #{userName}, '%') </if> <if test="phone != null and phone != ''"> AND phone = #{phone} </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND cr_time >= #{startTime} </if> <if test="endTime != null"> AND cr_time <= #{endTime} </if> </where> ORDER BY cr_time DESC</select>2、动态更新
<update id="updateSelective"> UPDATE t_user <set> <if test="userName != null and userName != ''"> user_name = #{userName}, </if> <if test="phone != null"> phone = #{phone}, </if> <if test="email != null"> email = #{email}, </if> up_time = NOW(), </set> WHERE id = #{id}</update>3、动态插入
<insert id="insertSelective"> INSERT INTO t_user <trim prefix="(" suffix=")" suffixOverrides=","> <if test="userName != null"> user_name, </if> <if test="phone != null"> phone, </if> <if test="email != null"> email, </if> cr_time, </trim> <trim prefix="VALUES (" suffix=")" suffixOverrides=","> <if test="userName != null"> #{userName}, </if> <if test="phone != null"> #{phone}, </if> <if test="email != null"> #{email}, </if> NOW(), </trim></insert>四、新人常见错误
1、Integer类型判断错误
<!-- 错误:Integer类型加了空字符串判断 --><if test="status != null and status != ''"> AND status = #{status}</if>
<!-- 正确 --><if test="status != null"> AND status = #{status}</if>2、忘记加AND或逗号
<!-- 错误:忘记加AND --><where> <if test="userName != null"> user_name = #{userName} <!-- 少了AND --> </if> <if test="phone != null"> AND phone = #{phone} </if></where>
<!-- 正确:第一个条件也要加AND,where标签会自动去掉 --><where> <if test="userName != null"> AND user_name = #{userName} </if> <if test="phone != null"> AND phone = #{phone} </if></where>3、批量操作没处理好
<!-- 错误:批量插入格式不对 --><insert id="insertBatch"> INSERT INTO t_user (user_name, phone) VALUES <foreach collection="list" item="user"> (#{user.userName}, #{user.phone}) <!-- 没有分隔符 --> </foreach></insert>
<!-- 正确:加上separator --><insert id="insertBatch"> INSERT INTO t_user (user_name, phone) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.phone}) </foreach></insert>4、foreach没有判空
新人常见错误!如果list为空,
<foreach>不会生成任何内容,SQL就会变成不合法的语法,直接报错。
<!-- 错误:没有判空,list为空时SQL变成 INSERT INTO t_user (user_name, phone) VALUES ← 后面啥都没有,语法错误! --><insert id="insertBatch"> INSERT INTO t_user (user_name, phone) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.phone}) </foreach></insert>
<!-- 错误:同样的问题,list为空时SQL变成 WHERE id IN ← 后面没有值,语法错误! --><select id="listByIds" resultType="User"> SELECT * FROM t_user WHERE id IN <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach></select>正确做法:在Service层判空,list为空时不调用Mapper方法:
@Overridepublic void insertBatch(List<UserDto> userList) { // 在Service层判空,避免传空集合到Mapper if (CollectionUtils.isEmpty(userList)) { return; } // 分批处理,每批不超过1000条 int batchSize = 1000; for (int i = 0; i < userList.size(); i += batchSize) { int end = Math.min(i + batchSize, userList.size()); userMapper.insertBatch(userList.subList(i, end)); }}总结:
<foreach>一定要配合判空使用。建议在Service层判断,集合为空直接return或抛异常,不要让空集合传到Mapper里。
五、新人避坑指南
| 坑 | 正确做法 |
|---|---|
| Integer判断加空字符串 | Integer只判断null |
| String判断只判断null | 还要判断空字符串 |
| 第一个if条件不加AND | 要加AND,where标签会自动去掉 |
| set里最后没逗号 | 要加逗号,set标签会自动去掉 |
| foreach没写separator | 批量操作要加分隔符 |
| foreach没有判空 | 在Service层判空,集合为空时不调用Mapper,避免SQL语法错误 |
| 复制粘贴不改参数名 | 仔细检查参数名是否正确 |
六、调试技巧
1、开启SQL日志
MyBatis项目:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplMyBatis-Plus项目:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl注意区分: MyBatis和MyBatis-Plus只是配置前缀不同,一个是
mybatis:,一个是mybatis-plus:。看看项目用的是哪个就配哪个。
开启日志后,控制台会打印实际执行的SQL,检查是否符合预期。
2、使用MyBatis Log Free插件(推荐)
在IDEA中安装 MyBatis Log Free 插件,可以把MyBatis日志中带
?占位符的SQL直接还原成完整的、可执行的SQL,复制到Navicat里直接运行排查问题。
安装方式: IDEA → Settings → Plugins → 搜索 MyBatis Log Free → Install

如果安装失败:

在 Appearance & Behavior->System Settings->HTTP Proxy 路径中勾选 Auto-detect proxy settings 后再重新 install 插件 MyBatis Log Free

使用步骤:
-
开启SQL日志(上面的第1步)
-
调用接口后,控制台会打印类似这样的日志:
==> Preparing: SELECT * FROM t_user WHERE user_name = ? AND status = ?==> Parameters: 张三(String), 1(Integer) -
打开IDEA底部的 MyBatis Log Free 面板(Tools → MyBatis Log Free)
-
插件会自动还原成完整SQL:
SELECT * FROM t_user WHERE user_name = '张三' AND status = 1
mybatislogfree-4 -
复制这条SQL,直接粘贴到Navicat执行,排查数据和条件是否正确
新人必装! 不装这个插件的话,你需要手动把
?和参数一一对应替换,参数多了很容易搞错。有了这个插件一键还原,效率提升很多。
七、终极建议
工作中90%的动态SQL就是if + where的组合,把这个练熟就够了。
遇到不确定的情况,开启SQL日志,看实际生成的SQL是什么样的,然后调整。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!














