前言
很多开发在写 MyBatis/MyBatis-Plus XML 动态 SQL 时,经常遇到<if test="xxx != ''">、<if test='xxx != ""'>两种写法,看似只是引号不同,运行时却出现判断失效、参数匹配异常、SQL 报错、空字符串误判等诡异 BUG。本文结合 OGNL 表达式底层原理,讲清单双引号本质区别、各自适用场景、高频踩坑案例与标准规范写法。
一、底层核心:MyBatis test 判断使用 OGNL 表达式
<if test="判断条件"> 内部不是普通 SQL,是 OGNL 表达式,引号解析规则由 OGNL 控制:
双引号 " ":OGNL 中代表字符串常量
单引号 ' ':OGNL 中代表单个字符常量
这是所有坑的根源,很多人误以为单双引号只是包裹字符串,实际 OGNL 对二者定义完全不同。
二、两种写法基础对比
1. 双引号写法(推荐标准写法)
xml
<!-- 正确:"" 代表空字符串 "" -->
<if test="username != ''">
AND username = #{username}
</if>
含义:判断入参username不等于空字符串,符合日常业务判断逻辑。
2. 单引号写法(极易踩坑)
xml
<!-- 错误示范:'' 代表两个字符常量,不是空字符串 -->
<if test='username != ""'>
AND username = #{username}
</if>
当你在外层用单引号包裹整个 test,内部双引号才是字符串;如果写成 <if test="username != ''"> 内部单引号,逻辑完全错乱。
三、高频踩坑场景与问题复现
场景 1:<if test="name != ''"> 单引号判断空字符串失效
错误代码:
xml
<!-- 致命问题:'' 在OGNL里是char字符,ASCII值32,不是空字符串 -->
<if test="name != ''">
AND name = #{name}
</if>
问题现象:
前端传空字符串 "" 时,判断依然成立,SQL 拼接 AND name = '',查询结果不符合预期;
原理:
'' = 两个单引号 = OGNL 识别为字符对象,类型为Character;
"" = 双引号 = OGNL 识别为字符串对象 String;
字符串"" 和 字符'' 类型不同,永远不相等,判断恒为 true。
场景 2:外层单引号包裹 test,内部使用双引号(可行但不推荐)
xml
<!-- 语法可行,但可读性差,容易混淆 -->
<if test='phone != "" and phone != null'>
AND phone = #{phone}
</if>
解析逻辑:
外层' ' 是 XML 属性包裹符;
内层"" 才是 OGNL 字符串常量,判断逻辑正常;
缺点:多层引号嵌套,修改时极易写错引号,团队统一规范不建议。
场景 3:判断数字 / 字符常量混淆报错
需求:判断类型不等于字符1错误写法:
xml
<!-- 报错:类型不匹配,int和Character无法对比 -->
<if test="type != '1'">
正确写法(数字用双引号字符串):
xml
<if test="type != "1">
报错原因:'1' 是单个字符,实体字段 type 为 Integer,数字与字符对比 OGNL 抛出类型转换异常。
场景 4:同时判 null 和空字符串,引号混用导致全量查询
错误代码:
xml
<if test="nickname != null and nickname != ''">
AND nickname = #{nickname}
</if>
问题:nickname 传空字符串时,nickname != '' 恒成立,拼接无效条件,查出全部数据。
四、衍生业务问题案例
案例 1:模糊查询误匹配空字符串,全表扫描
业务:根据用户名模糊搜索,为空时不拼接条件错误 XML:
xml
<select id="listUser" resultType="User">
SELECT id,username,phone FROM t_user
<where>
<if test="username != ''">
username LIKE CONCAT('%',#{username},'%')
</if>
</where>
</select>
异常表现:前端不传用户名(空串),SQL 依然拼接 username LIKE '%%',查出所有用户,无分页性能灾难。修复:将''改为""。
案例 2:新增接口非必填字段,单引号判断导致更新覆盖空值
更新逻辑:昵称不为空才更新错误代码:
xml
<update id="updateUser">
UPDATE t_user
<set>
<if test="nickname != ''">nickname = #{nickname},</if>
age = #{age}
</set>
WHERE id = #{id}
</update>
问题:前端传 nickname="",代码误以为有值,执行nickname = '',清空原有昵称数据。
五、标准规范写法(统一团队代码,杜绝引号 BUG)
规范 1:统一使用双引号判断空字符串(最优)
xml
<!-- 标准通用写法,同时判null、空串 -->
<if test="username != null and username != ''">
AND username = #{username}
</if>
规范 2:判断单个字符常量用单引号(仅字符场景)
xml
<!-- 判断字符类型字段,使用单引号 -->
<if test="sex != 'M'">
AND sex = #{sex}
</if>
规范 3:判断数字统一双引号
xml
<if test="status != "0">
AND status = #{status}
</if>
规范 4:外层禁止使用单引号包裹 test 属性
不推荐:
xml
<if test='name != ""'></if>
统一标准:所有test=""外层固定双引号。
六、补充拓展:OGNL 引号额外小知识
OGNL 中 'abc' 非法:单引号只能包裹单个字符,多字符必须用双引号 "abc";
如果需要字面量单引号,使用转义 \';
MyBatis 先解析 XML 属性,再执行 OGNL 表达式,两层解析容易造成引号冲突;
MyBatis-Plus 自带的QueryWrapper代码块不存在该问题,仅 XML 动态 SQL 有引号坑。
七、总结避坑清单
判断空字符串:必须写 != "",禁止 != '';
判断单个字符:可用单引号 'Y'/'N'/'M';
判断数字、多字符字符串:统一双引号 "1"、"admin";
test 属性外层永远使用双引号 test="",不混用单引号外层;
所有字符串判断必须同时加 != null,防止空指针;
历史代码批量排查:全局检索 != '' 统一替换为 != ""。
文末
MyBatis 动态 SQL 引号问题属于高频隐形 BUG,不会直接抛异常,但会造成数据查询、更新逻辑错乱,线上极易产生业务故障。统一团队 XML 引号书写规范,能从根源规避这类低级问题,提升 SQL 稳定性。