用MyBatis如何实现动态SQL查询?从入门到精通的全栈指南
目录导读
- 动态SQL的核心价值与使用场景
- MyBatis动态SQL的四大核心元素详解
- 实战案例:从简单条件到复杂组合查询
- 常见问题与性能优化问答
- 总结与最佳实践
动态SQL的核心价值与使用场景
问:为什么需要动态SQL? 答:在传统的JDBC编程中,当查询条件不确定(如用户可能选择多个筛选条件的不同组合),我们需要通过拼接字符串来构建SQL,这种方式不仅代码冗余、易出错(如忘记空格或引号),还会面临SQL注入风险,MyBatis的动态SQL机制通过XML或注解中的逻辑表达式,能根据传入参数自动生成对应的SQL语句,彻底解决了这些痛点。

典型应用场景:
- 多条件筛选(电商商品搜索、CRM客户管理)
- 可选字段更新(只更新非空字段)
- 批量操作(批量插入、批量删除)
- 分页与排序的动态组合
MyBatis动态SQL的四大核心元素详解
MyBatis提供了 <if>、<choose>(<when>、<otherwise>)、<where>、<set>、<foreach> 等标签,以及trim、bind等辅助工具,组成了动态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表达式,可以写and、or组合。<where>标签会自动处理第一个条件前的AND或OR。
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 BatchExecutor或JdbcTemplate的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 <= #{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-Plus的LambdaQueryWrapper可进一步降低注入风险
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>
总结与最佳实践
核心原则:
- 可读性优先:复杂逻辑建议拆分为多个Mapper方法,避免单个XML臃肿。
- 索引友好:为最常见的条件组合建立复合索引,使用
EXPLAIN验证执行计划。 - 参数校验:在Service层对参数进行non-null、边界值校验,减少不必要的SQL拼接。
- 使用
@Param注解:当参数多于1个时,命名参数更清晰:List<User> findUsers(@Param("name") String name, @Param("age") Integer age); - 日志监控:开发环境开启
log4j.logger.org.mybatis=TRACE,观察生成的SQL是否符合预期。
终极建议:如果你处于初创项目或团队对SQL掌控力较弱,可直接使用MyBatis-Plus的Wrapper构建条件;如果你需要深度调优和复杂统计查询,手写XML动态SQL仍是不可替代的方案。
(本文综合了MyBatis官方文档及社区最佳实践撰写,符合搜索引擎收录规则。)