
MyBatis 的if test判断失效绝对是我见过最多、也最冤的 MyBatis 问题之一。为什么说冤因为很多时候问题根本不在于 SQL 写错也不在于参数传错而是 test 属性的引号在 XML 和 OGNL 两层规则之间出了岔子导致动态 SQL 没有按预期拼接。我自己最早踩这个坑的时候第一反应是去查参数值、查日志SQL折腾了一个多小时最后才发现是引号嵌套把 test 表达式截断了。今天把单双引号混用这颗雷彻底拆干净搞清楚 XML 属性配对规则和 OGNL 字符串表示的区别你就能少走很多弯路。这篇内容适合所有用 MyBatis 写动态 SQL 的开发者尤其是刚接触if标签、对它又爱又恨的新手。1. 问题现场引号混用到底发生了什么1.1 最常见的翻车写法先说两种一写就炸的写法。if teststatus Y这是典型的外双内双。test 属性值本来应该由一对双引号包裹可内部又出现了一对双引号。XML 规范规定属性值靠配对引号界定范围解析器读到teststatus 时就认为第一个属性已经结束了后面的Y对 XML 来说全是非法杂散内容于是直接抛解析异常。if teststatus Y这是外单内单原理跟上面一模一样只是把外层的双引号换成了单引号。XML 解析器在第二个单引号处提前判定属性值结束剩下的Y变成了游离文本整个 mapper 文件直接解析失败。为什么这种写法会反复出现我观察下来主要原因是大家写 Java 的时候字符串默认用双引号写 SQL 的时候字符串默认用单引号两个习惯同时被带进 test 属性就容易外双内双、外单内单。尤其是从代码生成模板、历史遗留代码或者网上摘来的片段里复制时引号风格往往五花八门稍微一改就坏了。还有一种写法容易让人误判if teststatus Y这个其实是合法写法XML 外层用单引号内部字符串用双引号OGNL 也能正常解析。但它不是团队习惯后续维护的人下意识想把内层双引号改成单引号一下就变成错误写法。所以我个人强烈建议团队里不要用这种合法但别扭的写法统一用标准姿势少留隐患。1.2 报错与静默失效的两种表现引号问题导致的条件失效实际上有两种完全不同的现象排查思路也不一样。第一类是启动报错。mapper XML 在应用启动阶段就会被解析一旦 test 属性引号嵌套错误XML 解析立刻失败。常见报错信息包括元素类型 if 必须后跟属性规范、Attribute value expected、The content of elements must consist of well-formed character data等等。这类问题其实还算好排查因为应用起不来报错堆栈里通常能定位到某个 mapper 文件的某一行。第二类是静默失效。项目能启动接口也能调用但 SQL 日志里就是少了某个 where 条件查出来的数据比预期多或者更新影响行数不对。这种问题非常折磨人因为没有任何异常抛出来只有业务数据不对劲。导致静默失效的根因不止一种参数类型不匹配、OGNL 表达式操作符写错、变量名拼错都可能但排查顺序里一定要把 test 表达式内容放进去。这里我特别想纠正一个认知误区只要外层引号和字符串引号类型不同XML 层面就是合法的OGNL 大概率也能解析。真正导致报错的是外层引号与内部字符串引号撞在了一起也就是外双内双或外单内单。所以单双引号混用这个说法并不完全精确更准确说是引号配对错误导致表达式被截断。2. 为什么引号混用会让条件失效2.1 XML 属性值的引号配对规则要彻底理解这个坑得先搞清楚 XML 属性值的基本规则属性值必须用引号包围这个引号可以是双引号也可以是单引号如果最外层用了双引号属性值内部就不能再出现未转义的双引号但可以出现单引号反过来如果最外层用了单引号内部就不能再出现未转义的单引号但可以出现双引号。这个规则用一张表可以看得很清楚最外层引号内部直接使用单引号内部直接使用双引号双引号合法不需要特殊处理非法必须写成quot;单引号非法必须写成apos;合法不需要特殊处理放在 MyBatis 的场景里if test...的 test 就是一个普通 XML 属性必须遵守这套规则。很多开发者写的时候脑袋里想的是这个 test 是 Java 代码里的字符串于是顺理成章用 Java 的双引号习惯去套结果就踩了 XML 的坑。反过来SQL 里面写字符串条件的时候习惯用单引号比如AND status Y这个单引号在 XML 解析器眼里就是普通字符所以只要外层用双引号包着 test内层单引号随便写完全不会冲突。这也是为什么外层双引号、内层单引号会成为 MyBatis 社区最主流的写法。2.2 OGNL 表达式的字符串表示规则MyBatis 的 test 属性值使用的是 OGNL 表达式。OGNL 本身对字符串常量的定义比较宽松单引号和双引号都能表示字符串比如Y和Y在 OGNL 里都代表字符串。这个宽松特性反而是很多混乱的源头。如果项目里同时出现单引号字符串和双引号字符串那么在写 XML 属性时就必须想清楚我外层用的是哪种引号内层用哪种引号才不会冲突一旦在同一个表达式里出现外层单引号、内层又用单引号XML 就会截断表达式出现外层单引号、内层用双引号虽然合法但可读性很差。我建议团队里把 OGNL 字符串常量固定为单引号不要用双引号。理由很简单XML 属性值最常用双引号内部字符串用单引号可以跟 XML 外层完全错开SQL 里写字符串字面量也习惯用单引号OGNL 单引号字符串又和 Java 开发者对 char/String 的直觉接近。三个场景全用同一个符号脑子不用来回切换。反过来如果某天某个人在 Java 代码里拼接 XML为了省事把外层 XML 属性改成单引号内层的 OGNL 字符串又使用单引号立马就是嵌套错误。2.3 各种引号组合的合法性对比我把实际开发中出现过的各种 test 写法汇总成了下面这张表方便你对照检查写法XML 解析OGNL 解析结论teststatus Y合法合法推荐写法teststatus Y合法合法可用但不推荐teststatus Y非法属性被截断不进入 OGNL直接报错teststatus Y非法属性被截断不进入 OGNL直接报错teststatus quot;Yquot;合法合法可用但可读性差teststatus apos;Yapos;合法合法可用但可读性差从这张表可以得出一个结论真正推荐的做法只有第一行外层双引号内层单引号。后面几行要么有隐患要么就是靠 XML 实体转义硬解读起来费劲完全没有必要。至于quot;和apos;这种实体写法我在真实项目里见过有人用比如 test 里要判断字符串却写成teststatus quot;Yquot;能跑但任何接手代码的人看到都会想骂人。写 SQL 的人本来就要同时读 SQL、XML、Java 三种上下文实体转义再插一脚可读性直接崩溃。3. 正确写法与通用规范3.1 最稳妥的写法外层双引号内层单引号为了避免团队里反复出现引号问题我建议直接把规范定死if test外层一律使用双引号OGNL 字符串常量一律使用单引号数字、布尔值不加引号。不需要讨论不需要例外。看几个实际例子select idqueryOrder resultMaporderMap SELECT * FROM t_order where if testorderType ! null and orderType A AND order_type A /if if testamount ! null and amount gt 100 AND amount gt; 100 /if if testbuyerName ! null and buyerName ! AND buyer_name #{buyerName} /if if testdeleted false AND deleted 0 /if /where /select逐个看orderType A是字符串比较外层双引号包着 test内层用单引号包着 A完全避开 XML 属性冲突OGNL 正常识别字符串。amount gt 100是数字比较这里我用的是 OGNL 的gt操作符而不是。原因很直接在 XML 里虽然一般允许出现但gt;转义看多了容易眼花gt简洁且不需要任何转义。OGNL 支持lt、gt、lte、gte这些操作符优先级和语义都比较直观推荐在 test 表达式里多用。buyerName ! 是判断非空字符串。外层双引号内层是两个单引号在 XML 里完全合法。如果这段代码被人改成了外层单引号testbuyerName ! null and buyerName ! 就会立刻解析失败这也是为什么我一直强调不要突破外层双引号这个约定。deleted false是布尔比较不需要引号直接写false。3.2 需要外层单引号的特殊场景你可能想问那外层单引号是不是完全不能用也不是但它通常出现在手拼 XML 字符串的场景里。比如在 Java 代码里写一个函数动态拼接出一段包含if的 XML 片段。Java 字符串本身用双引号包围内部如果要写 XML 属性值紧跟一个双引号会很别扭所以有人会把 XML 属性值改成单引号String ifFragment if testorderType ! null and orderType \A\AND order_type A/if;这里 Java 字符串外层是双引号XML 属性 test 外层是单引号OGNL 字符串用\A\转义最终得到的 XML 内容是if testorderType ! null and orderType A能跑但每次读这段代码都要在心里做一次Java 转义 XML 引号 OGNL 字符串三层解码非常累。更麻烦的是空字符串判断。如果你想要在 Java 拼接的 XML 片段里表达name ! 外层 XML 属性已经用了单引号内层 OGNL 字符串再想用单引号就冲突了。你不得不用apos;然后 Java 字符串里还要写amp;apos;层层转义代码直接没法看。所以我的建议是不要在 Java 代码里手拼这种带复杂if的 XML 片段。如果项目确实需要动态拼接优先考虑把 SQL 片段放在 mapper XML 文件里或者用注解 SQL 的script方式组织再不行就用更现代的模板方式。手拼 XML 字符串看着自由实际上是把 XML 本来的结构化优势全丢了早晚会变成引号事故现场。3.3 其他容易导致 if 失效的隐形陷阱引号问题只是让 test 失效的入口之一排查时一定要顺带检查下面几类问题否则你刚修好引号条件还是不出来。第一逻辑操作符用错。test 表达式里写很容易踩坑因为 XML 里是特殊字符必须写成amp;amp;才不会报错。很多人写的时候没注意结果 XML 解析直接失败。为了避免这个问题OGNL 表达式里直接使用and、or、not不要用、||、!语义清晰还不用纠结 XML 转义。第二小于号必须转义或者换操作符。在 test 里写age 18在 XML 里会被认为是标签开始解析必然报错。你可以在 XML 里写age lt; 18也可以像我一样直接用age lt 18。我倾向于后者因为不需要记忆转义规则代码也更整齐。第三字符串和数字类型不匹配。如果status是 String 类型你写status 1OGNL 可能不会帮你做类型转换导致比较结果永远是 false。数字比较就直接写数字字符串比较就带上引号不要混着来。类似的问题还有 Integer 参数和 String 参数互相比较建议在 Java 层先把类型统一。第四null和空字符串的语义区分。name ! null只能排除 null过滤不了空字符串name ! 能过滤空串但让 null 参数也会被过滤掉。如果业务上 null 和空串都算没值要写成name ! null and name ! 。这种判断本身没错但很容易被后续维护的人简化错尤其是引号写法不规范的时候。4. 排查技巧与实战记录4.1 三步定位问题遇到 test 条件不生效我建议按下面三步走能省掉大量无效排查。第一步先看现象。项目启动阶段报错基本就是 XML 结构问题直接顺着报错信息去找 mapper 文件和行号。启动正常但 SQL 日志缺失条件说明问题出在表达式求值阶段需要继续往下查。第二步开启 MyBatis SQL 日志。在日志里找到对应接口的Preparing和Parameters对比实际生成的 SQL 里有没有你想拼接的条件片段。如果条件片段没出现说明if的判断结果是 false如果片段出现了但值不对那可能是参数赋值问题跟引号关系不大。第三步把 test 表达式原样复制出来单独验证。可以看引号是否配对也可以写个最笨的测试用 OGNL 表达式直接对参数 Map 求值。比如项目中已经有 OGNL 依赖的前提下可以写一个单元测试MapString, Object params new HashMap(); params.put(status, Y); Object result Ognl.getValue(status Y, params); System.out.println(result);如果输出 true说明表达式本身没问题那就是 XML 层面的引号或转义问题如果输出 false就要回头看参数类型、变量名和操作符。这里有个提醒MyBatis 对 OGNL 的使用是经过包装的直接用Ognl.getValue不一定能覆盖 MyBatis 的所有特性比如内建参数、绑定变量等。但对于纯字符串比较这种简单场景用来验证表达式语法足够有效。4.2 实际案例复盘分享一个我真实遇到的排查过程很典型。某次联调同事反馈某个查询接口的数据不对多查了一批订单。我打开 SQL 日志发现 where 条件里少了order_type A这个片段。第一反应是查 Controller 层入参请求参数里确实传了orderTypeA没问题。又怀疑是不是实体字段没映射上检查字段名也对不上问题。最后打开 mapper XML看到的if是这样写的if testorderType ! null and orderType A注意看test 外层用了单引号内层字符串也用单引号。XML 解析器把属性值截断成了orderType ! null and orderType 后面那个A直接游离在属性之外。这种写法在 XML 解析阶段就会报错项目实际上根本没有正常启动后续的查询结果不对其实是旧版本代码在运行。同事说条件不生效实际是这个 mapper 文件压根没被正确加载。这个案例里问题的表象和根因之间隔着一层 XML 解析如果只看业务逻辑怎么查都查不到引号头上来。这也是我为什么总跟团队强调test 表达式里的引号是结构化问题不是风格问题。再补一个静默失效案例。有人想在 test 里比较字符串却忘了给字符串加引号if testorderType ! null and orderType AOGNL 拿到A之后不会把它当成字符串而是当成一个变量去解析找不到变量就返回 null最终表达式求值为 false条件块被静默跳过。SQL 日志里看不到任何异常但 where 条件和预期不一致。这类问题虽然不算引号混用但和引号问题一起构成了 test 表达式的常见雷区排查时建议一起检查。4.3 规避建议经历了几次深夜排查之后我总结了一套预防措施现在基本能把这些坑挡在代码评审阶段。第一把引号约定写进团队规范。if test外层一律双引号OGNL 字符串一律单引号数字和布尔不加引号。这条规则一条就够解决大部分引号问题。第二代码评审时专门盯 mapper XML。我在评审 MyBatis 相关变更时会花 30 秒扫一遍所有if test的引号配对确认没有外双内双、外单内单的情况。这个动作成本极低效果极好。第三能用生成器生成 mapper 就不手写。很多代码生成工具生成的if都是标准写法人工改动次数越多越容易引入引号事故。第四打开 MyBatis 的 SQL 日志成为默认习惯。每次联调看到一条 SQL先跟预期对一下 where 条件不要等到数据对不上才开始排查。第五CI 里加一道 XML 校验。可以在构建流程中增加 mapper XML 的格式校验很多 IDE 也有静态检查插件能在编译期发现 XML 解析错误把问题提前暴露。结尾踩过几次坑之后我给自己定了一条强制原则凡是写if test不管是简单判断还是嵌套判断一律先写成外层双引号、内层单引号写完再扫一眼有没有配对错误。这条原则看起来简单但确实帮我省掉了大量无意义的排查时间。如果你现在正被某个消失的 WHERE 条件折磨别急着往参数和 SQL 里找先看看 test 的引号是不是已经在某一层断掉了。再分享一个我个人的小习惯遇到引号问题我会把 test 的完整内容复制到记事本用最笨的办法数一遍引号数量——外层是两个同类型的引号内层字符串是两两成对的单引号几秒钟就能定位问题。这种排查方法不高级但比盯着 IDE 反复纠结快得多。