一、动态SQL的前世今生

记得刚接触MyBatis时,每次在XML里手写大量重复SQL的痛苦吗?特别是当业务需求需要根据条件拼接不同SQL片段时,传统的拼接方式既容易出错又难以维护。直到遇到了动态SQL标签,就像发现了新大陆一般。

动态SQL的核心价值在于:用标签代替代码逻辑,使SQL语句既能保持灵活性,又能获得良好的可维护性。举个简单例子,当我们要实现一个根据不同查询条件筛选用户的功能时,传统做法需要在Java代码中拼接字符串,而现在只需要在XML映射文件中使用几个标签就能优雅实现。

二、核心标签实战解析

2.1 if标签:条件处理的万能钥匙

技术栈: MyBatis 3.5.6 + MySQL 8.0 + Java 8

<!-- 用户多条件查询 -->
<select id="searchUsers" resultType="User">
    SELECT * FROM users
    <where>
        <!-- 姓名模糊查询 -->
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <!-- 年龄范围查询 -->
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
        <if test="maxAge != null">
            AND age &lt;= #{maxAge}
        </if>
    </where>
    ORDER BY create_time DESC
</select>

这个示例展示了多个if标签的典型用法:

  • where标签自动处理首条条件的AND问题
  • 支持类型检查(null判断)和空字符串校验
  • 使用转义符号处理特殊字符(&lt;代替<)

关联技术点: 当使用LIKE查询时,MySQL的CONCAT函数可以避免SQL注入风险,相较于直接在Java代码中拼接%符号更为安全。

2.2 choose-when-otherwise:三选一的条件选择

<!-- 订单状态筛选器 -->
<select id="findOrders" resultType="Order">
    SELECT * FROM orders
    <where>
        <choose>
            <!-- 优先处理特殊状态 -->
            <when test="status == 'urgent'">
                status = 'pending' AND priority = 'high'
            </when>
            <!-- 处理常规状态 -->
            <when test="status != null">
                status = #{status}
            </when>
            <!-- 默认筛选未完成的订单 -->
            <otherwise>
                status NOT IN ('completed', 'canceled')
            </otherwise>
        </choose>
    </where>
</select>

这个选择结构的独特之处在于:

  1. 按顺序判断when条件,执行第一个匹配的分支
  2. otherwise作为默认选项
  3. 支持复杂条件表达式组合

2.3 foreach标签:批量操作的利器

典型场景1:批量插入

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

典型场景2:IN查询

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

高级技巧: 当处理百万级批量插入时,建议结合<foreach><transaction>,并设置batch执行模式。MySQL的max_allowed_packet参数需要适当调整,避免出现数据包过大的异常。

三、关联技术深度剖析

3.1 预编译语句与SQL注入

MyBatis的动态SQL在生成最终语句时,始终使用预编译参数:

-- 生成的prepareStatement示例
SELECT * FROM users WHERE name LIKE ? AND age >= ?

这种机制天然防止了SQL注入,但要注意在${}直接拼接的场景(非动态SQL标签使用场景)仍需谨慎。

3.2 动态SQL与缓存机制

二级缓存的key生成策略会考虑完整的SQL语句,因此动态SQL的条件变化会导致不同的缓存条目。建议对变化频繁的查询关闭缓存:

<select id="searchUsers" resultType="User" useCache="false">
    ...
</select>

四、技术细节黑匣子

4.1 条件判断的隐式转换

test表达式中,MyBatis会自动进行类型转换:

<!-- 字符串'0'和数字0判断要特别注意 -->
<if test="flag != null and flag != 0">

4.2 空格处理机制

动态SQL的智能空格处理:

<trim prefix="WHERE" prefixOverrides="AND">
    <!-- 自动去除多余的AND -->
</trim>

但要注意手动拼接的<script>标签内需要保证空格正确:

<script>
SELECT * FROM table
<where>...</where>
</script>

五、实战避坑指南

5.1 空集合判断陷阱

当处理<foreach>时,空集合需要特殊处理:

<if test="!ids.isEmpty()">
    ...
</if>

5.2 大数据量分页优化

在大数据量中使用动态SQL分页:

<select id="searchLargeData">
    SELECT * FROM table
    <include refid="dynamicConditions"/>
    LIMIT #{offset}, #{pageSize}
</select>

建议配合逻辑分页(游标方式)使用,而不是传统的物理分页。

六、性能优化四重奏

  1. 合理使用缓存:静态条件查询开缓存,动态条件关缓存
  2. 索引优化:EXPLAIN分析生成的SQL执行计划
  3. 批量操作:与Spring事务结合实现批处理
  4. 防止笛卡尔积:动态关联查询时注意连接条件

七、应用场景全景图

7.1 典型应用案例

  • 后台管理系统的高级搜索
  • 电商平台的多维度商品筛选
  • 大数据报表的动态字段选择
  • 权限系统的动态数据过滤

7.2 技术选型对比

场景 MyBatis动态SQL JPA Criteria
简单条件查询 ★★★★☆ ★★☆☆☆
复杂逻辑组合 ★★★★☆ ★★★☆☆
原生SQL优化需求 ★★★★★ ★★☆☆☆
快速开发迭代 ★★★☆☆ ★★★★★

八、技术边界的探索

8.1 与Spring Data JPA的混合使用

在Spring Boot项目中可以同时集成MyBatis和JPA:

@Repository
public class HybridRepository {
    @PersistenceContext
    private EntityManager em;
    
    @Autowired
    private SqlSessionTemplate sqlSession;
}

8.2 自定义动态标签

通过实现LanguageDriver扩展自定义标签:

public class CustomLanguageDriver implements LanguageDriver {
    // 实现自定义标签解析逻辑
}

九、核心技术总结

优点:

  1. 灵活度媲美原生SQL
  2. 可维护性优于代码拼接
  3. 类型安全机制完善
  4. 与MyBatis生态无缝集成

缺点:

  1. 复杂逻辑的可读性下降
  2. 调试困难(需要查看生成SQL)
  3. 过度使用可能导致XML臃肿

最佳实践:

  • 单个Mapper的SQL行数控制在200行以内
  • 复杂的动态逻辑拆分到<sql>片段
  • 配套使用MyBatis Generator生成基础代码
  • 结合PageHelper实现物理分页