MyBatis 支持动态 SQL 吗?
结论先行:支持。MyBatis 用一组 XML 标签(if、choose、where、set、trim、foreach、bind)配合 OGNL 表达式,在 SQL 解析阶段根据入参拼出不同的 SQL。它解决的是「一条语句适配多种查询条件」,而不是运行时拼接字符串。
1. 动态 SQL 的执行时机
Mapper 接口调用
└─ MappedStatement.getBoundSql(param)
└─ DynamicSqlSource.getBoundSql()
├─ 解析 <if>/<where>… 生成 SqlNode 树(启动时已完成)
├─ apply():按入参求值,拼接成最终 SQL 文本
└─ 生成 BoundSql{sql, parameterMappings, parameterObject}
注意:标签解析在启动时完成一次,apply 在每次执行时按参数求值。所以动态 SQL 的代价是每次拼 SQL,不是每次解析 XML。
2. 常用标签一览
| 标签 | 作用 | 备注 |
|---|---|---|
<if test="..."> | 条件成立才拼入 | test 是 OGNL 表达式 |
<choose>/<when>/<otherwise> | 多选一 | 类似 switch |
<where> | 自动加 WHERE,并去掉开头多余的 AND/OR | 比手写 where 1=1 干净 |
<set> | 自动加 SET,并去掉末尾多余的逗号 | 用于动态 UPDATE |
<trim> | 自定义前后缀与裁剪规则 | where/set 的底层实现 |
<foreach> | 遍历集合,拼 IN (...) 或批量 VALUES | 注意 open/separator/close |
<bind> | 绑定变量到上下文 | 常用于 LIKE 拼通配符 |
<sql>/<include> | SQL 片段复用 | 可配合 <property> 传参 |
3. 一段完整的例子
<select id="search" resultType="OrderVO">
SELECT id, user_id, amount, status, created_at
FROM t_order
<where>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="s" open="(" separator="," close=")">
#{s}
</foreach>
</if>
<if test="keyword != null and keyword != ''">
<bind name="likeKeyword" value="'%' + keyword + '%'"/>
AND (remark LIKE #{likeKeyword} OR order_no LIKE #{likeKeyword})
</if>
<if test="beginTime != null">
AND created_at >= #{beginTime}
</if>
<if test="endTime != null">
AND created_at < #{endTime}
</if>
</where>
<choose>
<when test="sort == 'amount'">ORDER BY amount DESC</when>
<otherwise>ORDER BY id DESC</otherwise>
</choose>
</select>
配套的 Mapper 方法与参数对象:
public interface OrderMapper {
List<OrderVO> search(@Param("userId") Long userId,
@Param("statusList") List<Integer> statusList,
@Param("keyword") String keyword,
@Param("beginTime") LocalDateTime beginTime,
@Param("endTime") LocalDateTime endTime,
@Param("sort") String sort);
}
动态 UPDATE 则用 <set>:
<update id="updateSelective">
UPDATE t_order
<set>
<if test="amount != null">amount = #{amount},</if>
<if test="status != null">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if>
</set>
WHERE id = #{id}
</update>
<set> 会去掉最后一个多余逗号;如果所有条件都不成立,语句会变成 UPDATE t_order WHERE id = ?,直接语法错误——所以业务上要保证至少有一个字段会更新。
4. OGNL 的常见写法与坑
| 写法 | 含义 | 坑 |
|---|---|---|
name != null and name != '' | 非空且非空串 | 只写 name != '' 时 null 也会通过 |
list.size() > 0 | 集合非空 | 也可写 list != null and !list.isEmpty() |
type == 'A' | 字符串比较 | 单字符会被 OGNL 当成 char,需写成 "A".equals(type) 或 type == "A" |
id in (1,2,3) | 集合判断 | 值多时性能差,优先 foreach |
@java.lang.Math@abs(x) | 静态方法 | 可用但可读性差,不建议 |
5. 常见追问
追问一:<where> 和 WHERE 1=1 有什么区别?
功能等价,<where> 更干净:它只在有子条件时才输出 WHERE,并自动裁掉开头的 AND/OR;1=1 则永远输出,且对索引优化器无影响但可读性差。生产代码推荐 <where>。
追问二:<foreach> 拼 IN 有数量限制吗?
有。MySQL 的 max_allowed_packet 限制 SQL 文本大小,Oracle 的 IN 列表上限 1000 个。几百以上的 ID 应该分批查询或改用临时表 join。
追问三:动态 SQL 会影响预编译缓存吗?
会。不同条件组合生成不同 SQL 文本,数据库要分别解析。条件分支多的语句会撑大执行计划缓存,必要时用 <choose> 收敛组合数。
追问四:注解方式能写动态 SQL 吗?
可以,用 <script> 包裹:@Select("<script>SELECT ... <if test='id != null'>AND id = #{id}</if></script>")。但可读性差,复杂语句还是写 XML。
追问五:<include> 能传参吗?
能。<sql id="base_columns">${alias}.id, ${alias}.name</sql> 配合 <include refid="base_columns"><property name="alias" value="o"/></include>,注意这里用的是 ${},属性值必须是可信常量。
小结
动态 SQL 的价值是把「条件组合」从 Java 字符串拼接里解放出来,交给声明式标签和 OGNL 求值。用好它的关键是三件事:条件判空要判全(null 与空串)、<where>/<set> 代替手工裁剪、<foreach> 控制集合规模。