告别if嵌套噩梦:MyBatis动态SQL的choose/when/otherwise实战指南
在后台管理系统开发中,我们经常遇到需要根据多种查询条件动态构建SQL的场景。比如用户可能选择按订单状态筛选,或者按时间范围查询,甚至需要根据不同用户类型访问不同的数据表。传统做法是堆砌大量if标签,结果代码变得像意大利面条一样难以维护。今天我们就来聊聊如何用MyBatis的choose/when/otherwise标签优雅解决这个问题。
1. 为什么需要choose/when/otherwise?
想象一下这样的场景:电商后台需要实现一个订单查询功能,支持以下筛选条件:
- 按订单状态(待付款/待发货/已完成)
- 按订单类型(普通/秒杀/团购)
- 按时间范围(今日/本周/本月)
如果用传统if标签实现,代码可能会长这样:
<select id="findOrders" resultType="Order"> SELECT * FROM orders <where> <if test="status != null"> AND status = #{status} </if> <if test="type != null"> AND type = #{type} </if> <if test="timeRange == 'today'"> AND create_time >= CURDATE() </if> <if test="timeRange == 'week'"> AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) </if> <!-- 更多if条件... --> </where> </select>这种写法存在几个明显问题:
- 条件优先级不明确:所有条件都是AND关系,无法实现"要么A要么B"的逻辑
- SQL冗余:当多个条件互斥时,生成的SQL可能包含不必要的判断
- 可读性差:随着条件增多,代码会变得难以理解和维护
2. choose/when/otherwise的基本用法
choose/when/otherwise组合类似于Java中的switch-case结构,让我们看个改造后的例子:
<select id="findOrders" resultType="Order"> SELECT * FROM orders <where> <choose> <when test="timeRange == 'today'"> AND create_time >= CURDATE() </when> <when test="timeRange == 'week'"> AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) </when> <otherwise> AND create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) </otherwise> </choose> <if test="status != null"> AND status = #{status} </if> </where> </select>这个例子展示了choose标签的几个关键特点:
- 互斥执行:只会执行第一个满足条件的when分支
- 默认分支:所有when都不满足时执行otherwise
- 组合使用:可以与其他动态SQL标签(如if)配合使用
3. 高级应用场景
3.1 多表查询动态选择
在分库分表场景下,我们经常需要根据某个字段值选择不同的表:
<select id="findUserOrders" resultType="Order"> SELECT * FROM <choose> <when test="userType == 'VIP'"> vip_orders </when> <when test="userType == 'ENTERPRISE'"> enterprise_orders </when> <otherwise> normal_orders </otherwise> </choose> WHERE user_id = #{userId} </select>3.2 复杂条件优先级处理
考虑一个商品搜索场景,我们希望:
- 优先按关键词搜索
- 无关键词时按分类筛选
- 既无关键词也无分类时展示推荐商品
<select id="searchProducts" resultType="Product"> SELECT * FROM products <where> <choose> <when test="keyword != null"> AND (name LIKE CONCAT('%',#{keyword},'%') OR description LIKE CONCAT('%',#{keyword},'%')) </when> <when test="categoryId != null"> AND category_id = #{categoryId} </when> <otherwise> AND is_recommended = 1 </otherwise> </choose> <if test="minPrice != null"> AND price >= #{minPrice} </if> </where> ORDER BY <choose> <when test="sortBy == 'price'">price</when> <when test="sortBy == 'sales'">sales_volume</when> <otherwise>create_time DESC</otherwise> </choose> </select>4. 常见问题与最佳实践
4.1 性能优化建议
- 避免过度使用:只在真正需要互斥逻辑时使用choose,简单条件仍用if
- 条件顺序:将最可能命中的条件放在前面
- 组合索引:确保where条件能够利用索引
4.2 常见错误排查
<!-- 错误示例1:when条件重叠 --> <choose> <when test="status == 1">...</when> <when test="status >= 1">...</when> <!-- 这个条件永远不会执行 --> </choose> <!-- 错误示例2:缺少otherwise --> <choose> <when test="type == 'A'">...</when> <when test="type == 'B'">...</when> <!-- 当type为C时,整个choose块不会生成任何SQL --> </choose>4.3 与其他标签的配合
| 标签组合 | 适用场景 | 示例 |
|---|---|---|
| choose + where | 条件查询 | <where><choose>...</choose></where> |
| choose + set | 条件更新 | <set><choose>...</choose></set> |
| choose + foreach | 批量操作 | <foreach><choose>...</choose></foreach> |
5. 实际项目中的经验分享
在最近的一个供应链管理系统中,我们遇到了一个复杂的产品筛选需求。用户需要能够:
- 优先按产品编码精确匹配
- 其次按产品名称模糊搜索
- 最后按产品分类浏览
最初我们使用了嵌套的if标签,结果XML文件膨胀到300多行,维护起来非常痛苦。重构为choose结构后,代码量减少了40%,逻辑也变得清晰明了:
<select id="findProducts" resultType="Product"> SELECT * FROM products <where> <choose> <when test="productCode != null"> AND code = #{productCode} </when> <when test="productName != null"> AND name LIKE CONCAT('%',#{productName},'%') </when> <when test="categoryId != null"> AND category_id = #{categoryId} </when> </choose> AND is_active = 1 </where> </select>另一个实用技巧是在otherwise中使用变量默认值:
<select id="getUserList" resultType="User"> SELECT * FROM users ORDER BY <choose> <when test="orderBy != null">${orderBy}</when> <otherwise>create_time DESC</otherwise> </choose> LIMIT <choose> <when test="limit != null">#{limit}</when> <otherwise>20</otherwise> </choose> </select>