MyBatis 动态 SQL,通过 XML (如 <if>、<foreach> 等)实现灵活的 SQL 拼接。

本结将详细讲解 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 &gt;= 18
      </otherwise>
    </choose>
  </where>
</select>

✅ 只会匹配一个条件,适合"互斥"场景。


四、<where> 标签:智能处理 WHERE 子句

自动:

  • 去掉开头的 ANDOR
  • 如果内部无内容,则不生成 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>:底层万能工具
相关推荐
weixin_448771724 小时前
SpringBoot默认日志配置文件 logback.xml(log4j+logback)
xml·spring boot·logback
合作小小程序员小小店7 小时前
桌面开发,在线%幼儿教育考试管理%系统,基于eclipse,java,swing,mysql数据库
java·数据库·sql·mysql·eclipse·jdk
⑩-8 小时前
苍穹外卖Day(8)(9)
java·spring boot·mybatis
朝新_9 小时前
Spring事务和事务传播机制
数据库·后端·sql·spring·javaee
寒山李白10 小时前
Mybatis使用教程之XML配置方式实现增删改查
xml·java·mybatis
q***05611 小时前
Spring 中使用Mybatis,超详细
spring·tomcat·mybatis
百***349511 小时前
Python连接SQL SEVER数据库全流程
数据库·python·sql
Violet_YSWY11 小时前
我就用mybatis作为与数据库交互,但我想用orm,最好的实现方案是啥
数据库·mybatis·交互
hygge99917 小时前
Spring Boot + MyBatis 整合与 MyBatis 原理全解析
java·开发语言·经验分享·spring boot·后端·mybatis