跳到主要内容

动态 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
  • 自动去除开头多余的 ANDOR

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 关键字,同时去除开头多余的 ANDOR。避免手动拼接时出现 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 注入?

答案

  1. 参数传值始终使用 #{} 而非 ${}
  2. 必须使用 ${} 时(表名、列名),在 Java 层做白名单校验
  3. 使用 MyBatis 提供的动态 SQL 标签(<where><if> 等)代替手动拼接

相关链接