1. 动态SQL中的分支选择逻辑
在MyBatis动态SQL体系中,choose/when/otherwise结构相当于编程语言中的switch-case-default语句,它提供了一种优雅的多条件分支处理机制。这个结构特别适合处理"多选一"的业务场景,比如根据不同的查询条件组合生成不同的SQL片段。
我在实际项目中经常遇到这样的需求:一个查询接口需要支持多种可选参数,但每次查询只使用其中部分参数。比如用户管理系统中,可能同时存在ID、手机号、邮箱等多种用户标识方式,但每次查询只需要使用其中一种标识。这时候choose结构就能完美解决这个问题。
1.1 基础语法解析
标准的choose/when/otherwise语法结构如下:
xml复制<choose>
<when test="条件表达式1">
SQL片段1
</when>
<when test="条件表达式2">
SQL片段2
</when>
<otherwise>
默认SQL片段
</otherwise>
</choose>
这个结构的工作原理是:
- 按顺序评估每个
when元素的test条件 - 执行第一个评估为
true的when块中的SQL - 如果所有
when条件都不满足,则执行otherwise块(如果有)
重要提示:
otherwise块是可选的,如果没有提供且所有when条件都不满足,则不会生成任何SQL片段。
1.2 与if语句的关键区别
很多初学者容易混淆choose和if的用法,这里我总结几个关键区别点:
| 特性 | choose/when/otherwise | if |
|---|---|---|
| 执行模式 | 多选一(只执行第一个匹配的) | 多选多(所有符合条件的都执行) |
| 默认分支 | 支持otherwise默认分支 | 无默认分支 |
| 适用场景 | 互斥条件 | 独立条件 |
| 条件评估顺序 | 严格按顺序评估 | 可并行评估 |
举个例子,在用户查询场景中:
- 如果业务要求"优先按ID查,没有ID再按手机号查,最后按邮箱查",应该用
choose - 如果业务要求"同时按所有提供的条件组合查询",应该用多个
if
2. 深度应用与实战技巧
2.1 复杂条件表达式编写
when元素的test属性支持丰富的OGNL表达式,可以组合出非常灵活的条件判断。以下是一些实用技巧:
多条件组合:
xml复制<when test="name != null and name.contains('张')">
AND name LIKE '%张%'
</when>
集合判断:
xml复制<when test="ids != null and !ids.isEmpty()">
AND id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
方法调用:
xml复制<when test="@com.example.StringUtils@isValid(code)">
AND security_code = #{code}
</when>
类型安全比较:
xml复制<when test="age != null and age gt 18">
AND adult_flag = 1
</when>
经验之谈:在XML中写复杂表达式时,建议先在Java代码中测试好表达式逻辑,确保其返回预期的布尔值。特别是涉及字符串操作或类型转换时,容易因为空指针或类型不匹配导致意外结果。
2.2 性能优化建议
虽然choose结构很灵活,但不当使用可能影响SQL性能:
-
条件顺序优化:将最可能匹配的条件放在前面,减少评估次数。比如用户查询中,ID查询比姓名查询更精确,应该优先处理。
-
避免过度嵌套:深层嵌套的
choose结构会降低SQL可读性和维护性。建议超过3层时考虑重构。 -
索引友好设计:确保
when块中的SQL条件能够利用数据库索引。例如:xml复制<when test="startTime != null and endTime != null"> <!-- 好的写法:可以利用时间索引 --> AND create_time BETWEEN #{startTime} AND #{endTime} </when> -
缓存考虑:不同的条件组合会生成不同的SQL,可能导致MyBatis缓存效率降低。对于高频查询,可以考虑固定查询模式。
2.3 与其它动态SQL元素的组合
choose可以与其他动态SQL元素灵活组合,实现更复杂的逻辑:
与where元素组合:
xml复制<where>
<choose>
<when test="deptId != null">
AND dept_id = #{deptId}
</when>
<otherwise>
AND dept_id IN (SELECT id FROM department WHERE status = 1)
</otherwise>
</choose>
</where>
与set元素组合(用于更新):
xml复制<set>
<choose>
<when test="newPassword != null">
password = #{newPassword},
</when>
<otherwise>
password_reset = 1,
</otherwise>
</choose>
</set>
与foreach元素嵌套:
xml复制<foreach collection="items" item="item">
<choose>
<when test="item.type == 'A'">
<!-- 处理A类型项 -->
</when>
<when test="item.type == 'B'">
<!-- 处理B类型项 -->
</when>
</choose>
</foreach>
3. 常见问题排查与解决方案
3.1 条件不生效问题排查
在实际开发中,经常遇到when条件看似满足但实际上不生效的情况。以下是系统化的排查步骤:
-
检查参数传递:确保参数确实传入了Mapper方法。可以在Mapper接口方法中打印参数或使用调试工具检查。
-
验证OGNL表达式:将
test表达式复制到Java代码中测试,确认其返回预期结果。例如:java复制// 测试OGNL表达式 boolean result = user.getName() != null && user.getName().contains("张"); -
检查特殊值处理:
- 空字符串
""与null是不同的 - 数字比较注意类型匹配(如
age gt 18中的age必须是数字类型) - 布尔值不要加引号(
test="flag"应该写成test="flag == true")
- 空字符串
-
查看生成的SQL:启用MyBatis日志,查看最终生成的SQL语句,确认条件是否按预期添加。
3.2 典型错误案例
案例1:字符串比较错误
xml复制<!-- 错误写法 -->
<when test="status == 'ACTIVE'">
<!-- 正确写法 -->
<when test='status == "ACTIVE"'>
<!-- 或 -->
<when test="'ACTIVE'.equals(status)">
案例2:数字类型不匹配
xml复制<!-- 错误写法:比较字符串和数字 -->
<when test="age > '18'">
<!-- 正确写法 -->
<when test="age gt 18">
案例3:空集合判断
xml复制<!-- 不够严谨的写法 -->
<when test="list != null">
<!-- 更严谨的写法 -->
<when test="list != null and !list.isEmpty()">
3.3 最佳实践总结
经过多个项目的实践,我总结了以下choose结构的最佳实践:
-
保持简洁:每个
when块尽量只包含必要的SQL片段,复杂逻辑考虑拆分成多个choose或提取到Java代码中处理。 -
添加注释:为每个条件块添加注释说明业务含义,特别是涉及复杂业务规则时。
-
防御性编程:总是考虑参数为
null或空的情况,避免运行时错误。 -
统一风格:团队内约定好条件表达式的编写风格(比如总是使用
gt而不是>),保持代码一致。 -
测试覆盖:为各种条件组合编写单元测试,特别是边界条件。
4. 高级应用场景
4.1 动态表名选择
在一些分表场景中,可以根据参数动态选择表名:
xml复制<select id="queryUser" resultType="User">
SELECT * FROM
<choose>
<when test="userType == 'VIP'">
user_vip
</when>
<when test="userType == 'NORMAL'">
user_normal
</when>
<otherwise>
user_guest
</otherwise>
</choose>
WHERE id = #{id}
</select>
注意:这种用法需要确保表名是安全的,避免SQL注入风险。如果表名来自用户输入,必须进行严格校验。
4.2 多策略排序
实现根据用户选择的不同排序策略:
xml复制<select id="queryProducts" resultType="Product">
SELECT * FROM product
ORDER BY
<choose>
<when test="sortBy == 'price'">
price ${order}
</when>
<when test="sortBy == 'sales'">
sales_count ${order}
</when>
<otherwise>
create_time DESC
</otherwise>
</choose>
</select>
4.3 条件字段动态选择
根据参数决定使用哪个字段作为查询条件:
xml复制<select id="search" resultType="Item">
SELECT * FROM items
WHERE
<choose>
<when test="code != null">
item_code = #{code}
</when>
<when test="barcode != null">
barcode = #{barcode}
</when>
<otherwise>
name LIKE CONCAT('%', #{name}, '%')
</otherwise>
</choose>
</select>
4.4 批量操作中的差异化处理
在批量操作中,根据每个项的不同属性执行不同操作:
xml复制<update id="batchUpdate">
<foreach collection="items" item="item" separator=";">
UPDATE products
<set>
<choose>
<when test="item.discount != null">
price = price * #{item.discount},
discount_flag = 1
</when>
<otherwise>
price = #{item.newPrice}
</otherwise>
</choose>
</set>
WHERE id = #{item.id}
</foreach>
</update>
在实际项目中,我发现choose结构特别适合处理业务规则复杂的查询场景。比如在一个电商后台系统中,我们使用它实现了多维度订单查询:根据不同的查询条件组合(订单状态、支付方式、时间范围等),动态生成最优化的查询SQL,既保持了代码的整洁性,又确保了查询性能。