本结将详细讲解 MyBatis 的 动态 SQL 标签:
<if><choose>,<when>,<otherwise><where>,<set><foreach>(循环标签)<trim>
一、#{}:安全的参数绑定(推荐默认使用)
| 特性 | #{} |
${} |
|---|---|---|
| 底层机制 | 预编译(PreparedStatement) | 字符串拼接(直接替换) |
| 是否防 SQL 注入 | ✅ 安全 | ❌ 危险 |
| 参数类型处理 | 自动类型转换(如 int → Integer) | 原样字符串插入 |
| 适用场景 | 绝大多数情况(值替换) | 动态表名、列名、排序字段等元数据 |
| 生成的 SQL 示例 | WHERE id = ? |
WHERE id = 123 |
- MyBatis 将
#{xxx}转换为 JDBC 的?占位符。 - 参数通过
PreparedStatement.setXXX()方法传入。 - 数据库将其视为纯数据,不会解析为 SQL 代码。
示例:
<select id="getUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
实际执行的 SQL(预编译):
SELECT * FROM users WHERE id = ?
-- 参数: [123]
🔒 即使传入
id = "1 OR 1=1",也会被当作字符串'1 OR 1=1',不会破坏 SQL 结构。
${}:不安全的字符串替换(慎用!)
⚠️ 原理:
- MyBatis 直接将
${xxx}替换为参数的 字符串表示。 - 相当于手动拼接 SQL 字符串。
- 无任何转义或类型处理,极易导致 SQL 注入!
示例(危险!):
<select id="getUserByName" resultType="User">
SELECT * FROM users WHERE name = '${name}'
</select>
如果调用时传入:
userMapper.getUserByName("admin' --");
生成的 SQL:
SELECT * FROM users WHERE name = 'admin' --'
→ 攻击者可绕过验证!
什么时候必须用 ${}?
虽然危险,但在某些动态 SQL 元数据场景下无法避免:
场景 1:动态表名
<select id="selectFromTable" resultType="Map">
SELECT * FROM ${tableName}
</select>
场景 2:动态列名(如排序)
<select id="getUsersOrderBy" resultType="User">
SELECT * FROM users ORDER BY ${columnName} ${order}
</select>
场景 3:动态 LIMIT(部分数据库不支持 ?)
<select id="getUsersLimit" resultType="User">
SELECT * FROM users LIMIT ${offset}, ${limit}
</select>
💡 注意:MySQL 的
LIMIT在较新版本中已支持?,但旧版或某些数据库仍需${}。
如何安全使用 ${}?
由于 ${} 有注入风险,必须做严格校验!
✅ 安全实践:
1. 白名单校验
public List<User> getUsersByColumn(String column, String order) {
// 白名单:只允许特定列名和排序方式
Set<String> allowedColumns = Set.of("id", "name", "age");
if (!allowedColumns.contains(column)) {
throw new IllegalArgumentException("Invalid column: " + column);
}
if (!"ASC".equalsIgnoreCase(order) && !"DESC".equalsIgnoreCase(order)) {
throw new IllegalArgumentException("Invalid order: " + order);
}
return userMapper.getUsersOrderBy(column, order);
}
2. 使用 <bind> + #{} 替代(部分场景)
对于模糊查询,不要写:
<!-- ❌ 危险 -->
SELECT * FROM users WHERE name LIKE '%${name}%'
应写:
<!-- ✅ 安全 -->
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM users WHERE name LIKE #{pattern}
3. 避免用户直接控制 ${} 内容
永远不要让前端传入表名、列名等直接用于 ${}!
常见误区
❌ 误区 1:认为 ${} 只是"更快"
- 错!性能差异微乎其微,安全风险巨大。
❌ 误区 2:用 ${} 处理数字更方便
- 错!
#{age}会自动处理int/Integer,无需担心类型。
❌ 误区 3:MyBatis 会自动转义 ${
- 错!完全不会 !
${}就是原始字符串替换。
二、<if> 标签:条件判断
用于根据条件决定是否拼接某段 SQL。
示例:动态查询用户
<select id="findUsers" parameterType="User" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
⚠️ 注意:
WHERE 1=1是为了防止第一个条件不满足时 SQL 语法错误(但有更好的方式,见<where>)。
三、<choose>, <when>, <otherwise>:多条件分支(类似 switch)
只执行第一个 test 为 true 的 <when>,类似 Java 的 if-else if-else。
示例:按优先级查询
<select id="findUserByPriority" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="name != null and name != ''">
name = #{name}
</when>
<otherwise>
age >= 18
</otherwise>
</choose>
</where>
</select>
✅ 只会匹配一个条件,适合"互斥"场景。
四、<where> 标签:智能处理 WHERE 子句
自动:
- 去掉开头的
AND或OR - 如果内部无内容,则不生成
WHERE关键字
改进版 <if> 查询(推荐写法)
<select id="findUsers" parameterType="User" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
✅ 不再需要
WHERE 1=1!
五、<set> 标签:智能处理 UPDATE 的 SET 子句
自动:
- 去掉结尾多余的逗号
- 如果无更新字段,则不生成
SET
示例:动态更新用户信息
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>
✅ 安全避免
UPDATE users SET WHERE id = ?这类错误。
六、<foreach> 标签:循环(最常用在 IN 查询、批量插入)
属性说明:
| 属性 | 说明 |
|---|---|
collection |
要遍历的集合(List、数组、Map 的 key) |
item |
当前元素的变量名 |
index |
当前索引(可选) |
open |
开始符号(如 () |
close |
结束符号(如 )) |
separator |
分隔符(如 ,) |
场景 1:IN 查询(传入 List ids)
List<User> findByIds(@Param("ids") List<Long> ids);
<select id="findByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
🔑 注意:如果参数是 单个 List ,MyBatis 默认用
list作为 collection 名;但建议用@Param("ids")显式命名。
场景 2:批量插入(传入 List)
void batchInsert(@Param("users") List<User> users);
<insert id="batchInsert">
INSERT INTO users (name, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
生成 SQL:
INSERT INTO users (name, age) VALUES ('Alice', 25), ('Bob', 30)
场景 3:遍历 Map(较少用)
void updateByMap(Map<String, Object> params);
<update id="updateByMap">
UPDATE users
<set>
<foreach collection="entrySet()" index="key" item="value">
${key} = #{value},
</foreach>
</set>
WHERE id = #{id}
</update>
⚠️ 注意:
${key}是直接拼接(有 SQL 注入风险!),仅用于列名等元数据,且需严格校验。
七、<trim> 标签:通用前后缀处理(<where> 和 <set> 的底层实现)
示例:自定义 WHERE(等价于 <where>)
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
示例:自定义 SET(等价于 <set>)
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
✅
prefixOverrides:去除开头指定字符串✅
suffixOverrides:去除结尾指定字符串
八、其他实用标签(简要)
| 标签 | 用途 |
|---|---|
<bind> |
创建 OGNL 表达式变量(如模糊查询预处理) |
<sql> + <include> |
SQL 片段复用 |
<bind> 示例:
<select id="findByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM users WHERE name LIKE #{pattern}
</select>
<sql> 复用示例:
<sql id="userColumns">id, name, age</sql>
<select id="getAllUsers" resultType="User">
SELECT <include refid="userColumns" /> FROM users
</select>
九、OGNL 表达式注意事项
MyBatis 使用 OGNL(Object-Graph Navigation Language) 作为表达式语言。
- 字符串判空:
name != null and name != '' - 数字判空:
age != null - 集合判空:
roles != null and !roles.isEmpty() - 安全访问:
user?.name(MyBatis 3.4+ 支持)
十、最佳实践总结
| 场景 | 推荐写法 |
|---|---|
| 动态 WHERE | 用 <where> + <if> |
| 动态 UPDATE | 用 <set> + <if> |
| IN 查询 / 批量操作 | 用 <foreach> |
| 多条件互斥 | 用 <choose> |
| SQL 复用 | 用 <sql> + <include> |
| 模糊查询 | 用 <bind> 预处理,避免 SQL 注入 |
🔒 安全提示 :永远不要在动态 SQL 中使用
${}拼接用户输入!应使用#{}参数化。
总结
MyBatis 的动态 SQL 标签让复杂查询变得简洁、安全、可维护:
<if>:基础条件<choose>:多选一<where>/<set>:智能包裹<foreach>:循环利器(IN、批量)<trim>:底层万能工具