用MyBatis如何实现动态SQL查询?

wen java案例 68

用MyBatis如何实现动态SQL查询?从入门到精通的全栈指南

目录导读

  1. 动态SQL的核心价值与使用场景
  2. MyBatis动态SQL的四大核心元素详解
  3. 实战案例:从简单条件到复杂组合查询
  4. 常见问题与性能优化问答
  5. 总结与最佳实践

动态SQL的核心价值与使用场景

问:为什么需要动态SQL? 答:在传统的JDBC编程中,当查询条件不确定(如用户可能选择多个筛选条件的不同组合),我们需要通过拼接字符串来构建SQL,这种方式不仅代码冗余、易出错(如忘记空格或引号),还会面临SQL注入风险,MyBatis的动态SQL机制通过XML或注解中的逻辑表达式,能根据传入参数自动生成对应的SQL语句,彻底解决了这些痛点。

用MyBatis如何实现动态SQL查询?

典型应用场景:

  • 多条件筛选(电商商品搜索、CRM客户管理)
  • 可选字段更新(只更新非空字段)
  • 批量操作(批量插入、批量删除)
  • 分页与排序的动态组合

MyBatis动态SQL的四大核心元素详解

MyBatis提供了 <if><choose><when><otherwise>)、<where><set><foreach> 等标签,以及trimbind等辅助工具,组成了动态SQL的强大武器库。

1 <if>:基础条件判断

<select id="findUsers" 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>

注意test属性支持OGNL表达式,可以写andor组合。<where>标签会自动处理第一个条件前的ANDOR

2 <choose>:多条件分支(类似Java的switch)

<select id="findUserByCondition" resultType="User">
    SELECT * FROM users WHERE status = 1
    <choose>
        <when test="searchType == 'name'">
            AND name LIKE CONCAT('%', #{keyword}, '%')
        </when>
        <when test="searchType == 'email'">
            AND email = #{keyword}
        </when>
        <otherwise>
            AND phone = #{keyword}
        </otherwise>
    </choose>
</select>

应用场景:当需要根据不同类型执行不同查询条件时,choose比多个if更清晰。

3 <where><set>:智能处理前缀和固定语法

  • <where>:自动去除AND/OR前缀,无需担心首条件是否为空。
  • <set>:用于UPDATE语句,自动处理逗号,避免字段列表后的多余逗号。

更新示例

<update id="updateUser" parameterType="User">
    UPDATE users
    <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>会生成UPDATE users WHERE id = #{id},此时会报错——因此应确保至少有一个非空字段,或配合<where>使用业务校验。

4 <foreach>:集合遍历(批量操作核心)

<select id="findUsersByIds" resultType="User">
    SELECT * FROM users WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

属性说明

  • collection:传入的集合/数组参数名
  • item:循环变量名
  • open:循环开始符号(如)
  • close:循环结束符号(如)
  • separator:分隔符(如)

批量插入优化

<insert id="batchInsert">
    INSERT INTO users(name, age)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age})
    </foreach>
</insert>

注意:大批量插入时(>1000条),应使用MyBatis BatchExecutorJdbcTemplate的batch操作,防止SQL过长。


实战案例:从简单条件到复杂组合查询

案例1:多字段模糊搜索(支持排序和分页)

<select id="searchUsers" resultType="User">
    SELECT * FROM users
    <where>
        <if test="keyword != null and keyword != ''">
            AND (name LIKE CONCAT('%', #{keyword}, '%')
            OR email LIKE CONCAT('%', #{keyword}, '%'))
        </if>
        <if test="startAge != null">
            AND age >= #{startAge}
        </if>
        <if test="endAge != null">
            AND age &lt;= #{endAge}
        </if>
    </where>
    <if test="orderBy != null and orderBy != ''">
        ORDER BY ${orderBy}
    </if>
    <if test="offset != null and limit != null">
        LIMIT #{offset}, #{limit}
    </if>
</select>

注意${orderBy}使用直接替换(不能预编译),存在SQL注入风险。最佳实践:将排序字段硬编码为枚举或白名单,例如if (orderBy == "create_time")

案例2:多级嵌套条件(使用<trim>自定义逻辑)

<select id="complexSearch" resultType="Product">
    SELECT * FROM product
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="category != null">
            AND category_id = #{category}
        </if>
        <if test="priceRange != null">
            AND price BETWEEN #{priceRange.min} AND #{priceRange.max}
        </if>
        <if test="tags != null and tags.size() > 0">
            AND EXISTS (
            SELECT 1 FROM product_tag WHERE product_id = product.id
            AND tag_id IN
            <foreach collection="tags" item="tag" open="(" separator="," close=")">
                #{tag}
            </foreach>
            )
        </if>
    </trim>
</select>

<trim>可自定义前缀和后缀,以及需要覆盖的前缀/后缀,灵活度最高。


常见问题与性能优化问答

Q1:动态SQL拼接导致SQL长度过大怎么办? A:MyBatis有默认的SQL长度限制(如max_allowed_packet),对于<foreach>批量操作,建议分片处理(每次500-1000条),开启logImpl查看完整SQL,检查是否包含冗余条件。

Q2:<if>条件过多是否影响性能? A:解析环节的轻微消耗可忽略不计,真正的性能瓶颈在于生成的SQL是否走索引WHERE 1=1虽然方便,但可能导致索引失效(尤其是复合索引的最左前缀原则被破坏),建议用<where>替代1=1,并利用EXPLAIN分析SQL。

Q3:如何防止动态SQL中的SQL注入? A:除了不能用于用户输入(仅用于表名、列名白名单),还要注意:

  • 不要使用LIKE '%${keyword}%',应改用LIKE CONCAT('%', #{keyword}, '%')
  • 避免在order by后直接拼接用户参数,使用白名单校验
  • 使用MyBatis-PlusLambdaQueryWrapper可进一步降低注入风险

Q4:动态SQL支持存储过程调用吗? A:支持,通过<select>标签的statementType="CALLABLE",配合<if>条件传入参数。

<select id="callProcedure" statementType="CALLABLE">
    {CALL search_users(
        #{name, mode=IN},
        #{age, mode=IN},
        #{result, mode=OUT, jdbcType=CURSOR, resultMap=userMap}
    )}
</select>

总结与最佳实践

核心原则

  1. 可读性优先:复杂逻辑建议拆分为多个Mapper方法,避免单个XML臃肿。
  2. 索引友好:为最常见的条件组合建立复合索引,使用EXPLAIN验证执行计划。
  3. 参数校验:在Service层对参数进行non-null、边界值校验,减少不必要的SQL拼接。
  4. 使用@Param注解:当参数多于1个时,命名参数更清晰:
    List<User> findUsers(@Param("name") String name, @Param("age") Integer age);
  5. 日志监控:开发环境开启log4j.logger.org.mybatis=TRACE,观察生成的SQL是否符合预期。

终极建议:如果你处于初创项目或团队对SQL掌控力较弱,可直接使用MyBatis-PlusWrapper构建条件;如果你需要深度调优和复杂统计查询,手写XML动态SQL仍是不可替代的方案。


(本文综合了MyBatis官方文档及社区最佳实践撰写,符合搜索引擎收录规则。)

抱歉,评论功能暂时关闭!