MyBatis 支持动态 SQL 吗?

#Java#面试八股

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 &gt;= #{beginTime}
        </if>
        <if test="endTime != null">
            AND created_at &lt; #{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> 控制集合规模。

笔记加载中…