1. MyBatis动态SQL的本质与价值
动态SQL是MyBatis框架最核心的竞争力之一。想象你正在开发一个电商平台的商品搜索功能:用户可能根据商品名称、价格区间、分类等多个条件进行组合查询。如果为每种可能的条件组合都编写单独的SQL语句,那将是一场维护噩梦。这正是动态SQL要解决的核心痛点。
我在实际项目中见过最典型的反面案例:某金融系统中有个长达800行的存储过程,包含了各种if-else分支来处理不同的查询条件。而改用MyBatis动态SQL后,同样的功能只需50行清晰的XML配置。这种代码量的缩减不是简单的数字游戏,它直接带来了:
- 可维护性提升:条件逻辑可视化,不再隐藏在代码分支中
- 开发效率飞跃:新增查询条件只需添加一个标签,无需修改Java代码
- SQL注入风险降低:自动处理参数转义,避免手工拼接字符串
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态SQL的四大核心标签详解
2.1 标签的实战技巧
xml复制<select id="searchProducts" resultType="Product">
SELECT * FROM products
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="categoryIds != null and categoryIds.size() > 0">
AND category_id IN
<foreach collection="categoryIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
</select>
这里有几个关键经验:
- 始终使用
标签包裹条件,它会智能处理AND/OR前缀 - 字符串判断要同时检查null和空字符串,避免NPE风险
- 集合参数必须检查size,否则IN语句会导致语法错误
- 模糊查询使用#{}防止注入,不要用${}直接拼接
踩坑警示:我曾遇到一个线上事故,因为没有检查categoryIds的size,当传入空列表时生成的SQL变成"WHERE id IN ()"导致数据库报错。现在团队规范要求所有集合参数必须做空校验。
2.2 标签的巧妙运用
当需要实现类似switch-case的逻辑时:
xml复制<select id="getUserList" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="departmentId != null">
department_id = #{departmentId}
</when>
<when test="teamId != null">
team_id = #{teamId}
</when>
<otherwise>
status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
实际项目中我发现:
- 多个
条件的顺序就是判断优先级 相当于default case,建议总是保留 - 这种结构特别适合权限过滤等场景
2.3 标签的高阶用法
批量插入的经典模式:
xml复制<insert id="batchInsert">
INSERT INTO employees (name, email, dept_id) VALUES
<foreach collection="list" item="emp" separator=",">
(#{emp.name}, #{emp.email}, #{emp.deptId})
</foreach>
</insert>
但这里有个性能陷阱:MySQL默认允许的max_allowed_packet大小可能导致大批量插入失败。我的解决方案是:
- 在代码中自动分批次处理(每批500条)
- 添加rewriteBatchedStatements=true连接参数
- 对于超大批量考虑使用LOAD DATA INFILE替代
2.4 标签的更新艺术
动态更新时防止空字段覆盖:
xml复制<update id="updateUser">
UPDATE users
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="avatar != null">avatar = #{avatar}</if>
</set>
WHERE id = #{id}
</update>
特别注意:
- 最后一个条件不要加逗号,MyBatis会自动处理
- 建议所有更新操作都加上乐观锁版本号检查
- 对于敏感字段(如密码)应该单独处理,不要放在动态set中
3. 动态SQL的安全防御实战
3.1 #{}与${}的血泪教训
某次安全扫描发现的典型漏洞:
xml复制<!-- 危险写法 -->
ORDER BY ${sortField} ${sortDirection}
<!-- 正确写法 -->
ORDER BY
<choose>
<when test="sortField == 'price'">price</when>
<when test="sortField == 'sales'">sales</when>
<otherwise>id</otherwise>
</choose>
<if test="sortDirection == 'DESC'">DESC</if>
关键原则:
- 排序字段必须白名单校验
- 表名/列名等元数据不要用${}拼接
- 动态表名场景可以考虑使用SQL注入过滤器
3.2 防范批量操作风险
曾经有个惨痛案例:某同事编写的批量删除语句缺少条件限制,导致全表数据被清空。现在的团队规范要求:
- 所有批量操作必须带有业务条件限制
- 生产环境执行前先EXPLAIN确认影响范围
- 重要操作添加@Transactional注解
4. 性能优化进阶技巧
4.1 避免动态SQL的性能陷阱
某次性能分析发现的典型问题:
xml复制<select id="getReportData">
SELECT * FROM transactions
<where>
<if test="type != null">AND type = #{type}</if>
<!-- 十几个可选条件 -->
</where>
</select>
当条件组合很多时,会导致数据库无法有效使用索引。优化方案:
- 建立组合索引时把高筛选度的字段放前面
- 对常用查询路径建立单独的优化SQL
- 使用
预处理复杂条件:
xml复制<bind name="dateRange" value="startDate != null and endDate != null" />
<if test="dateRange">
AND create_time BETWEEN #{startDate} AND #{endDate}
</if>
4.2 大型动态SQL的维护策略
当动态SQL超过20个条件时:
- 按业务模块拆分成多个
片段 - 使用
引入公共部分 - 添加XML注释说明每个条件的业务含义
- 配套编写单元测试验证各种条件组合
5. 与MyBatis Plus的配合之道
虽然MyBatis Plus提供了LambdaQueryWrapper等编程式DSL,但在复杂场景下,XML动态SQL仍有不可替代的优势。我的混合使用经验:
- 简单CRUD用MyBatis Plus提高效率
- 复杂多表关联查询用XML维护可读性
- 通过@SelectProvider实现动态SQL与注解的融合
例如这个统计报表查询:
java复制@SelectProvider(type = ReportSqlBuilder.class, method = "buildReportSql")
List<ReportVO> getSalesReport(ReportQuery query);
其中SqlBuilder类可以自由组合Java代码和XML片段的优势。
6. 动态SQL的调试技巧
6.1 日志输出优化
在logback.xml中配置:
xml复制<logger name="org.mybatis" level="DEBUG"/>
然后在控制台可以看到实际执行的SQL语句。但要注意:
- 生产环境不要开启DEBUG日志
- 敏感数据需要脱敏处理
- 可以使用MyBatis的SQL美化插件格式化输出
6.2 单元测试验证
我习惯为每个动态SQL编写测试用例:
java复制@Test
void testSearchWithMultipleConditions() {
ProductQuery query = new ProductQuery();
query.setName("手机");
query.setMinPrice(1000);
query.setCategoryIds(Arrays.asList(1, 3, 5));
List<Product> results = productMapper.searchProducts(query);
assertFalse(results.isEmpty());
// 验证生成的SQL是否正确
String sql = ((SqlSessionFactory) factory).getConfiguration()
.getMappedStatement("com.mapper.ProductMapper.searchProducts")
.getBoundSql(query).getSql();
assertTrue(sql.contains("LIKE '%手机%'"));
assertTrue(sql.contains("price >="));
assertTrue(sql.contains("IN (1,3,5)"));
}
7. 企业级最佳实践
经过多个大型项目验证的规范:
- XML文件按模块划分,不超过500行/文件
- 所有动态SQL必须配套单元测试
- 禁止在SQL中出现业务逻辑判断
- 批量操作必须添加分页/批次限制
- 生产环境禁用${}的使用
- 定期使用SQL扫描工具检查潜在风险
在金融级项目中,我们还额外要求:
- 所有SQL变更需要DBA审核
- 动态SQL必须包含执行计划分析
- 关键查询添加熔断保护机制
- 建立SQL性能基线监控
这些经验可能看起来有些严格,但在处理百万级QPS的系统时,动态SQL的任何一个微小问题都可能被放大成生产事故。
