欢迎光临
我们一直在努力

MyBatis 动态 SQL 之 OGNL 表达式:从入门到避坑指南

MyBatis 动态 SQL 之 OGNL 表达式:从入门到避坑指南

前言 在 MyBatis 开发中,动态 SQL 是核心功能之一,而支撑动态 SQL 逻辑判断的正是 OGNL (Object-Graph Navigation Language) 表达式。

很多开发者容易将其与 JSP 中的 EL 表达式混淆。虽然两者语法相似,但 MyBatis 底层使用的是 OGNL 引擎。理解 OGNL 的类型转换机制、布尔值解析规则以及特殊字符处理,是写出健壮 SQL 的关键。

本文将系统总结 MyBatis 中 OGNL 表达式的常用写法,重点剖析那些“看似正常却暗藏杀机”的陷阱。


一、字符串处理的“坑”与技巧

字符串是业务中最常见的数据类型,但在 OGNL 中处理字符串比较时,类型匹配和空值判断极易出错。

1. Null 判断(基础)

最基础的判空操作,注意 null 是 OGNL 的关键字,无需加引号。

<!– 判断为 null –>
<if test="bookname == null">
AND book_name IS NULL
</if>

<!– 判断不为 null –>
<if test="bookname != null">
AND book_name IS NOT NULL
</if>

2. 空串判断(高频陷阱)

⚠️ 注意:在 Java 中,null 和 "" (空字符串) 是两个概念。在 OGNL 中,如果字段可能为空串,必须同时判断 != null 和 != '',否则当传入空字符串时,可能会抛出异常或逻辑错误。

<!– ✅ 推荐写法:排除 null 和 空串 –>
<if test="bookname != null and bookname != ''">
AND book_name = #{bookname}
</if>

<!– ❌ 错误逻辑示例:如果不等于空串,SQL 却去查等于空串的数据? –>
<!– 原常见错误:test="bookname != ''" 对应 SQL "book_name = ''" (逻辑矛盾) –>

3. 单字符类型陷阱(经典 Bug)

这是 MyBatis 中最著名的“坑”。在 OGNL 中,单引号包裹的单个字符(如 '1')会被解析为 Java 的 char 类型,而实体类中的字段通常是 String 类型。char 与 String 类型不匹配,导致恒为 false。

<!– ❌ 错误代码:'1' 被解析为 char,与 String 类型的 bookname 比较失败 –>
<if test="bookname == '1'">
AND book_name = '1'
</if>

<!– ✅ 正确写法 1:调用 toString() 将 char 转为 String –>
<if test="bookname == '1'.toString()">
AND book_name = #{bookname}
</if>

<!– ✅ 正确写法 2:使用双引号定义字符串 (XML 属性用单引号包裹) –>
<if test='bookname == "1"'>
AND book_name = #{bookname}
</if>

4. 调用字符串自身方法

OGNL 支持直接调用对象的实例方法,这使得我们可以进行模糊匹配的前置判断。

<!– 包含 "三" –>
<if test="bookname != null and bookname.indexOf('三') > -1">
AND book_name LIKE '%三%'
</if>

<!– 以 "记" 结尾 –>
<if test="bookname != null and bookname.endsWith('记')">
AND book_name LIKE '%记'
</if>

<!– 以 "水" 开头 –>
<if test="bookname != null and bookname.startsWith('水')">
AND book_name LIKE '水%'
</if>

5. 字符串转数组配合 foreach

当前端传来逗号分隔的字符串(如 "Java,Python,Go")时,可以直接在 OGNL 中调用 split 方法转为集合,供 <foreach> 使用。

<if test="booknames != null and booknames.length() > 0">
<!– 注意:split 返回的是数组,OGNL 可自动识别为集合 –>
<foreach item="item" index="index"
collection="booknames.split(',')"
open="AND book_name IN (" separator="," close=")">

#{item}
</foreach>
</if>


二、数字类型的布尔隐式转换(深度解析)

数字类型的判断看似简单,但 OGNL 有一套独特的**对象转布尔值(Interpreting Objects as Booleans)**规则,这是导致 id=0 查询失效的元凶。

1. OGNL 的布尔解析规则

根据 OGNL 官方规范,任何对象在布尔上下文中(如 <if test="…">)的解析规则如下:

  • Boolean: 直接取其值。
  • Number (int, double, BigDecimal 等):
    • 非零值

      \\rightarrow

      true

    • 零值 (0, 0.0)

      \\rightarrow

      false ⚠️

  • Character: 非零字符

    \\rightarrow

    true,零字符

    \\rightarrow

    false。

  • 其他对象: 非 null

    \\rightarrow

    true,null

    \\rightarrow

    false。

2. “0 值” 引发的血案

假设 id = 0,我们来看以下几种写法的执行结果:

<!– 场景:id = 0 –>

<!– 1. 显式数值比较 (✅ 安全) –>
<if test="id != null and id < 1">
<!– 结果:TRUE (0 < 1),SQL 执行 –>
AND id = 0
</if>

<!– 2. 隐式布尔判断 (❌ 危险) –>
<if test="id != null and id == true">
<!– 结果:FALSE (0 被解析为 false,false != true),SQL 不执行 –>
AND id = 2
</if>

<!– 3. 隐式布尔判断 (❌ 危险但符合规则) –>
<if test="id != null and id == false">
<!– 结果:TRUE (0 被解析为 false,false == false),SQL 执行 –>
<!– ⚠️ 这种写法极度不推荐,可读性差且易误导 –>
AND id = 3
</if>

<!– 4. 跨类型比较陷阱 (❌ 危险) –>
<if test="id != null and id == ''">
<!– 结果:TRUE!
原因:id(0) -> false; ''(空串) -> false; false == false 成立 –>

AND id = 4
</if>

3. 最佳实践

永远不要依赖数字的隐式布尔转换! 始终使用显式的数值比较运算符。

<!– ✅ 推荐:明确比较数值 –>
<if test="id != null and id != 0">
AND id != 0
</if>

<!– ✅ 推荐:使用 OGNL 符号别名 –>
<if test="id != null and id gt 0">
AND id > 0
</if>
<if test="id != null and id lte 100">
AND id <= 100
</if>

📝 常用比较符速查表
OGNL 符号等价运算符含义
eq == 等于
neq != 不等于
gt > 大于
gte >= 大于等于
lt < 小于
lte <= 小于等于

三、高级用法:调用静态方法

OGNL 允许直接在表达式中调用 Java 的静态方法,语法格式为:@全限定类名@方法名(参数)。

1. 使用内置工具类

例如使用 java.lang.Math 进行数值计算后判断:

<select id="selectByMathLogic" resultType="Book">
SELECT * FROM book
<where>
<!– 如果 id1 和 id2 的最大值小于 10 –>
<if test="@java.lang.Math@max(id1, id2) lt 10">
AND status = 1
</if>

<!– 如果 id1 和 id2 的最小值大于 3 –>
<if test="@java.lang.Math@min(id1, id2) gt 3">
AND status = 2
</if>
</where>
</select>

2. 调用自定义工具类

你可以调用自己项目中的工具类,例如日期格式化或加密校验:

<!– 假设 com.example.util.DateUtil 有一个 isValid 方法 –>
<if test="@com.example.util.DateUtil@isValid(dateStr)">
AND create_time >= #{dateStr}
</if>

⚠️ 注意事项:

  • 安全性:在生产环境中,需评估是否允许任意静态方法调用,防止恶意代码执行。
  • 性能:频繁调用静态方法会增加 OGNL 解析开销。对于复杂逻辑,建议在 Service 层预处理成布尔标志位(如 isMatch),再传给 Mapper。

  • 四、其他实用技巧补充

    1. Map 类型的取值

    如果传入的参数是一个 Map,OGNL 支持两种取值方式:

    • 直接取 Key:test="userName != null" (等价于 map.userName)
    • 显式取 Key:test="map['user-name'] != null" (当 Key 包含特殊字符如 – 时必须使用此法)

    2. 特殊字符转义

    在 XML 文件中,<, >, & 是保留字符。虽然在 test 属性中 MyBatis 通常能智能处理,但为了规范和避免解析错误,建议:

    • 使用 OGNL 别名(gt, lt, and, or)。
    • 或者使用 XML 实体转义:&lt; (<), &gt; (>), &amp; (&)。
    • 对于复杂的逻辑块,可以使用 <![CDATA[ … ]]> 包裹(通常用于标签内容,属性内较少用)。

    3. List/Set 集合判断

    判断集合是否有元素:

    <!– 推荐:显式判断 size –>
    <if test="list != null and list.size() > 0">

    </if>

    <!– 利用 OGNL 特性:非空集合视为 true –>
    <if test="list">

    </if>


    五、总结

    MyBatis 的 OGNL 表达式功能强大,但也因动态类型转换机制埋下了不少隐患。掌握以下核心原则,可避开 90% 的坑:

  • 术语准确:记住是 OGNL 不是 EL。
  • 字符串比较:单字符要用双引号或 .toString();判空要兼顾 null 和 ''。
  • 数字比较:严禁依赖 0 的隐式布尔转换,永远使用 gt, lt, == 0 等显式比较。
  • 静态方法:能用则用,但复杂逻辑建议上移至 Java 代码层。
  • 代码规范:优先使用 eq, gt 等别名代替符号,提高 XML 的可读性和兼容性。
  • 希望这篇总结能帮助你在编写 MyBatis 动态 SQL 时更加得心应手!


    参考源码示例:MyBatis Study on Gitee

    赞(0)
    未经允许不得转载:171主机测评 » MyBatis 动态 SQL 之 OGNL 表达式:从入门到避坑指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址