← 返回上一页

第2章: 动态 SQL

让 SQL 像 Java 一样有 if-else —— 按条件自动拼接

学习进度 0/6

为什么需要动态 SQL?

🔍 场景:电商搜索框

打开京东搜索页面,你可以按关键词搜、按价格区间筛、按品牌筛、按销量排序...
用户可能只填了其中 1 个条件,也可能填了 5 个条件。

如果没有动态 SQL,你得写多少种 SQL?

❌ 只搜关键词 → WHERE name LIKE ?
❌ 关键词+价格 → WHERE name LIKE ? AND price > ?
❌ 关键词+品牌 → WHERE name LIKE ? AND brand = ?
❌ 全部条件都填 → WHERE name LIKE ? AND price > ? AND brand = ? ...
2^5 = 32 种组合... 写 32 个 SQL?疯了!
✅ 动态 SQL:一个 XML 搞定所有组合!有什么条件就拼什么条件!

📁 项目准备:代码放在哪?怎么跑起来?

⚠️ 前置要求:

请先完成第 1 章的项目搭建(Spring Boot + MyBatis + MySQL)。本章在第 1 章的基础上修改已有文件

需要修改的文件

src/main/java/com/example/
├── Application.java           ← 不用改
├── entity/
│   └── User.java               ← 不用改
├── mapper/
│   └── UserMapper.java        ← 增加动态查询方法
└── controller/
    └── UserController.java    ← 增加搜索接口

src/main/resources/
├── application.yml            ← 不用改
└── mapper/
    └── UserMapper.xml         ← 增加动态 SQL 语句

怎么测试动态 SQL?

// 📄 在 UserMapper.java 中增加方法: List<User> searchUsers(@Param("username") String username, @Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge); // 📄 在 UserController.java 中增加接口: @GetMapping("/search") public List<User> search(@RequestParam(required=false) String username, @RequestParam(required=false) Integer minAge, @RequestParam(required=false) Integer maxAge) { return userMapper.searchUsers(username, minAge, maxAge); }
# 📄 启动项目后,在浏览器或 Postman 中测试: # 启动:在 IDEA 中运行 Application.java 的 main 方法 # 只按用户名搜索 GET http://localhost:8080/search?username=张 # 只按年龄范围搜索 GET http://localhost:8080/search?minAge=25&maxAge=30 # 组合搜索 GET http://localhost:8080/search?username=张&minAge=20&maxAge=30 # 不传任何参数 → 查所有 GET http://localhost:8080/search # 观察 IDEA 控制台打印的 SQL,看动态拼接的效果!

动态 SQL 五大标签(记住这 5 个就够了)

🔀
<if>
如果条件成立,就拼接这段 SQL
≈ Java 的 if (...) { }
📍
<where>
自动加 WHERE,自动去掉多余的 AND/OR
解决 WHERE 1=1 的丑陋写法
🎯
<choose><when><otherwise>
多选一,只执行第一个匹配的条件
≈ Java 的 if-else if-else / switch
🔄
<foreach>
循环集合,用于 IN 查询和批量操作
≈ Java 的 for (item : list) { }
✏️
<set>
动态更新,自动去掉多余的逗号
只更新不为 null 的字段
🧹
<trim>
万能修剪器,自定义前缀/后缀的增删
where 和 set 底层都是用它实现的

⭐ <if> + <where> —— 最常用的组合

🍽️ 比喻:点餐时的"口味要求"

你对服务员说:"给我来份面"(基本 SQL)
如果你想加辣 → "要辣的"(拼上 AND spicy = true)
如果你要大碗 → "要大碗"(拼上 AND size = 'large')
如果你什么都不加 → 就是普通面(不加任何 WHERE 条件)

<!-- 📄 UserMapper.xml — 动态条件查询 --> <!-- ❌ 错误写法:WHERE 1=1 丑陋又不专业 --> <select id="findByCondition" resultType="User"> SELECT * FROM users WHERE 1=1 <if test="username != null"> AND username LIKE CONCAT('%', #{username}, '%') </if> </select> <!-- ✅ 正确写法:用 <where> 标签! --> <select id="findByCondition" resultType="User"> SELECT * FROM users <!-- <where> 标签的两个魔法: 1. 只有内部有内容时才加 WHERE 关键字 2. 自动去掉开头多余的 AND / OR --> <where> <!-- test 里写 Java 表达式,判断参数是否有值 --> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="email != null"> AND email = #{email} </if> <if test="age != null"> AND age = #{age} </if> </where> ORDER BY id DESC </select>

🎮 互动:动态查询 SQL 生成器

勾选不同的查询条件,实时看到 MyBatis 生成的 SQL:

按用户名模糊搜索
按邮箱精确查询
按年龄查询
⚡ MyBatis 生成的最终 SQL:
SELECT * FROM users ORDER BY id DESC
没有勾选任何条件 → 不会生成 WHERE 子句

其他常用标签

🎯 <choose> —— 多选一(类似 switch)

场景:搜索用户时,优先按用户名搜,没有用户名就按邮箱搜,都没有就返回所有成年人。

<!-- choose/when/otherwise = Java 的 if-else if-else --> <select id="findByPriority" resultType="User"> SELECT * FROM users <where> <choose> <!-- 优先按用户名搜 --> <when test="username != null"> username = #{username} </when> <!-- 用户名没填?那按邮箱搜 --> <when test="email != null"> email = #{email} </when> <!-- 都没填?返回所有成年用户 --> <otherwise> age >= 18 </otherwise> </choose> </where> </select> <!-- 等同于 Java 代码: if (username != null) { sql += "WHERE username = ?"; } else if (email != null) { sql += "WHERE email = ?"; } else { sql += "WHERE age >= 18"; } -->

🔄 <foreach> —— 循环(批量操作神器)

场景:购物车批量删除、批量导入用户、IN 查询等。

<!-- 📄 批量删除 — DELETE FROM users WHERE id IN (1, 3, 5) --> <delete id="deleteByIds"> DELETE FROM users WHERE id IN <!-- foreach 属性详解: collection = 参数名(List 用 "list",数组用 "array",或自定义名 @Param) item = 每一项的变量名 open = 循环开始前加的字符 close = 循环结束后加的字符 separator = 每项之间的分隔符 --> <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </delete> <!-- 📄 批量插入 — 一次插入多条数据 --> <insert id="batchInsert"> INSERT INTO users(username, email, age) VALUES <foreach collection="users" item="user" separator=","> (#{user.username}, #{user.email}, #{user.age}) </foreach> </insert>

🎮 互动:foreach 生成器

输入要删除的 ID 列表,看 foreach 生成的 SQL:
删除 ID:
DELETE FROM users WHERE id IN (请输入ID...)

✏️ <set> —— 动态更新(只更新有值的字段)

场景:用户编辑个人资料,只改了昵称没改邮箱,SQL 就只更新昵称。

<!-- <set> 标签的两个魔法: 1. 自动加 SET 关键字 2. 自动去掉最后多余的逗号 --> <update id="updateSelective"> UPDATE users <set> <!-- 只有传了 username 才更新 username 列 --> <if test="username != null"> username = #{username}, </if> <!-- 只有传了 email 才更新 email 列 --> <if test="email != null"> email = #{email}, </if> <!-- 只有传了 age 才更新 age 列 --> <if test="age != null"> age = #{age}, </if> </set> WHERE id = #{id} </update> <!-- 如果只传了 username: 生成 SQL → UPDATE users SET username = '张三' WHERE id = 1 ✅ <set> 自动去掉了最后那个多余的逗号! -->

🎮 互动:动态更新 SQL 生成器

勾选要更新的字段,看 <set> 生成的 SQL:
更新用户名
更新邮箱
更新年龄
UPDATE users -- 请勾选要更新的字段

实战:完整的用户搜索接口

把所有标签串起来,看一个真实的搜索功能怎么写:

// 📄 UserMapper.java — 接口定义 @Mapper public interface UserMapper { // 动态条件查询(SQL 在 XML 中) List<User> search(UserSearchParam param); // 批量删除(SQL 在 XML 中) int deleteByIds(@Param("ids") List<Long> ids); // 选择性更新(SQL 在 XML 中) int updateSelective(User user); }
// 📄 UserSearchParam.java — 搜索参数类 // 把所有搜索条件封装成一个对象,比一堆参数清晰 public class UserSearchParam { private String username; // 按用户名搜(可选) private String email; // 按邮箱搜(可选) private Integer minAge; // 最小年龄(可选) private Integer maxAge; // 最大年龄(可选) private String orderBy; // 排序字段(可选) // getter/setter 省略... }
<!-- 📄 UserMapper.xml — 完整的动态查询 --> <select id="search" resultType="User"> SELECT * FROM users <where> <!-- 用户名模糊搜索 --> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <!-- 邮箱精确搜索 --> <if test="email != null and email != ''"> AND email = #{email} </if> <!-- 年龄范围 --> <if test="minAge != null"> AND age >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{maxAge} </if> </where> <!-- 动态排序(⚠️ 排序字段用 ${} 因为是列名,不能用 #{}) --> <if test="orderBy != null"> ORDER BY ${orderBy} </if> <if test="orderBy == null"> ORDER BY id DESC </if> </select>
// 📄 UserController.java — 搜索接口 @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserMapper userMapper; // GET /api/users/search?username=张&minAge=20&maxAge=30 @GetMapping("/search") public List<User> search(UserSearchParam param) { // Spring 自动把 URL 参数绑定到 param 对象 return userMapper.search(param); } // DELETE /api/users/batch?ids=1,3,5 @DeleteMapping("/batch") public String batchDelete(@RequestParam List<Long> ids) { int count = userMapper.deleteByIds(ids); return "成功删除 " + count + " 条记录"; } }
⚠️ #{} 和 ${} 的区别(面试必问!)
  • #{} = 预编译占位符 ?防 SQL 注入,99% 情况用它
  • ${} = 字符串直接拼接,有注入风险,只用于列名、表名等不能用 ? 的地方

📝 学完检测

第 1 题:<where> 标签的核心作用是?
A. 替代 WHERE 关键字,没有其他作用
B. 自动加 WHERE + 自动去除开头多余的 AND/OR
C. 自动加 WHERE 1=1
D. 只能配合 if 使用
第 2 题:<foreach> 中 separator="," 的作用是?
A. 在循环开始时加逗号
B. 在循环结束时加逗号
C. 在每一项之间加逗号作为分隔符
D. 在 SQL 末尾加逗号
第 3 题:<choose><when><otherwise> 类似于 Java 的什么?
A. for 循环
B. if - else if - else
C. try - catch
D. while 循环

本章小结

← 上一章: MyBatis 入门 返回上一页