动态 SQL
问题
MyBatis 动态 SQL 是什么?有哪些常用标签?如何防止 SQL 注入?
答案
动态 SQL 标签一览
| 标签 | 用途 |
|---|---|
<if> | 条件判断 |
<choose>/<when>/<otherwise> | 多选一(类似 switch) |
<where> | 智能拼接 WHERE,自动去除多余 AND/OR |
<set> | 智能拼接 SET,自动去除多余逗号 |
<trim> | 自定义前缀/后缀及去除规则 |
<foreach> | 遍历集合(IN 查询、批量插入) |
<sql>/<include> | SQL 片段复用 |
if 条件判断
条件查询
<select id="listUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<where> 的作用:
- 只在子元素有内容时插入
WHERE - 自动去除开头多余的
AND或OR
choose / when / otherwise
类似 Java 的 switch-case,只匹配第一个满足的条件:
多选一
<select id="findUser" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">
AND id = #{id}
</when>
<when test="name != null">
AND name = #{name}
</when>
<otherwise>
AND status = 1
</otherwise>
</choose>
</where>
</select>
foreach 遍历
IN 查询
<select id="getUsersByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
批量插入
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
| 属性 | 说明 |
|---|---|
collection | 要遍历的集合(list/array/map 的 key) |
item | 当前元素的变量名 |
index | 当前索引 |
open | 前缀 |
close | 后缀 |
separator | 元素间分隔符 |
set 动态更新
动态更新
<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> 的作用:自动添加 SET 关键字,去除末尾多余的逗号。
trim 自定义
<where> 和 <set> 都是 <trim> 的特例:
trim 实现 where
<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
sql / include 片段复用
SQL 片段
<!-- 定义可复用的列名片段 -->
<sql id="userColumns">
id, name, age, email, status, create_time
</sql>
<select id="getById" resultType="User">
SELECT <include refid="userColumns"/> FROM user WHERE id = #{id}
</select>
<select id="listAll" resultType="User">
SELECT <include refid="userColumns"/> FROM user WHERE status = 1
</select>
#{} vs ${} —— SQL 注入防护
安全 vs 不安全
<!-- ✅ 安全:#{} 使用 PreparedStatement 预编译 -->
<select id="getByName" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
<!-- 执行:SELECT * FROM user WHERE name = ?(参数化查询) -->
<!-- ❌ 危险:${} 直接拼接字符串 -->
<select id="getByName" resultType="User">
SELECT * FROM user WHERE name = '${name}'
</select>
<!-- 如果 name = "'; DROP TABLE user; --" 会导致 SQL 注入 -->
什么时候才用
${}只有在表名、列名、ORDER BY 等无法预编译的场景才用 ${},且必须在代码层做白名单校验:
// 白名单校验
List<String> allowedColumns = List.of("name", "age", "create_time");
if (!allowedColumns.contains(sortColumn)) {
throw new IllegalArgumentException("非法排序字段");
}
常见面试问题
Q1: MyBatis 动态 SQL 有哪些标签?
答案:
<if> 条件判断、<choose>/<when>/<otherwise> 多选一、<where> 智能拼接 WHERE、<set> 智能拼接 SET、<trim> 自定义前缀/后缀、<foreach> 遍历集合、<sql>/<include> 片段复用。
Q2: #{} 和 ${} 的区别?
答案:
#{} 是预编译,使用 PreparedStatement 的 ? 占位符,防止 SQL 注入,99% 的场景应该用它。${} 是字符串拼接,直接将值插入 SQL,有注入风险,仅用于动态表名、列名、ORDER BY 等无法预编译的场景,使用时必须做白名单校验。
Q3: <where> 标签的作用?
答案:
<where> 智能拼接 WHERE 子句:当子元素有内容时自动添加 WHERE 关键字,同时去除开头多余的 AND 或 OR。避免手动拼接时出现 WHERE AND name = ? 或 WHERE 1=1 的问题。
Q4: foreach 的 collection 属性怎么传值?
答案:
- 单个
List参数:collection="list"或使用@Param("ids")后collection="ids" - 单个
Array参数:collection="array" - Map 中的集合:
collection="mapKey" - 推荐始终使用
@Param注解指定名称
Q5: 如何防止 MyBatis 的 SQL 注入?
答案:
- 参数传值始终使用
#{}而非${} - 必须使用
${}时(表名、列名),在 Java 层做白名单校验 - 使用 MyBatis 提供的动态 SQL 标签(
<where>、<if>等)代替手动拼接
相关链接
- MyBatis 动态 SQL 文档
- 执行流程 - ParameterHandler 如何设置参数