目录
前言
在 MyBatis 中,<if> 标签用于动态生成 SQL 查询条件。然而,在一些特定的场景下,<if> 标签的条件判断可能会出现意料之外的结果。例如,当传入的 Integer 参数为 0 时,条件判断可能不会如预期那样生效,穿非0时就正常,本文将深入探讨这个问题的原因,并结合源码分析提供解决方案。
问题背景
在一个校园运维平台项目中,有个设备安装位置管理模块,是维护了一些设备安装的位置,位置状态有正常状态和停用状态,正常状态用status = 0 而异常为1来表示。
某一天对正常位置进行筛选,关键问题来了,筛选正常的状态但是出现了停用的状态
出现这种问题时,我首先怀疑是否是 SQL 语句逻辑存在错误。接着,我检查了代码逻辑和 Mapper 文件中的 SQL 语句,但未发现明显的问题。
<select id="findPositionByNameAndStatus" resultType="com.ruoyi.system.domain.position.Position">
<include refid="selectPositionVO"></include>
<where>
<if test="po.name != null and po.name != ''">
AND name LIKE CONCAT('%', #{po.name}, '%')
</if>
<if test="po.status != null and po.status != ''">
AND status = #{po.status}
</if>
AND tenant_id = #{po.tenantId} and is_delete = 0 order by sort asc
</where>
</select>
既然sql语句没问题,是不是status没有正确的赋值传进来呢?通过debug发现status是正确赋值了
SQL 语句没有问题, status 的值也正常赋值。那么为什么查询正常位置时却能把停用的位置也查出来呢?接下来,需要检查执行的 SQL 是否正确。
在分析过程中,我发现当 status传入0时,SQL 语句中的 if条件未能正确处理,导致AND条件未能拼接进查询语句中。而当status传入1时,条件能正常拼接。
所以这是为什么呢?
经过排查,发现status是Integer类型,Integer类型是不需要跟空字符串比较的,在Mybatis底层中,判断if条件时是用OGNL表达式来判断的,当Integer类型的status为0的时候,OGNL 表达式处理 po.status != '' 时,可能在处理 Integer 类型值为0 时出现了问题。虽然从逻辑上 0 不等于 ''(空字符串),但在 OGNL 的处理机制中,0 和 '' 的比较可能导致了意外的结果,从而导致条件未能正确通过。
至于出现了什么意外结果,接下来跟着我深度解析mybatis的源码,先说结果,主要是Mybatis解析条件语句是通过OGNL表达式来判断的,这个表达式的底层会把 ''(空字符串)解析成一个double类型为0.0的值,而0也会被解析成0.0,而0.0与0.0自然是相等了,所以说这if条件是不成立的,因此就拼接不了AND语句,就发生了查询正常状态的位置出现停用状态 位置的现象了。
深入源码
首先先进入Mybatis动态构建sql的核心类DynamicSqlSource,它会通过解析Mybatis XML的配置来根据传入的参数来生成不同的SQL语句。
在这个核心类中它会调用rootSqlNode.apply的方法, 这个方法是用来解析我们动态标签的,比如<if>、<foreach>、<choose>等等这些动态标签,继续跟踪进去,就会调到IfSqlNode类,其中apply方法会调用evaluator.evaluateBoolean方法,这个方法主要用于处理 <if> 标签的实现类,它的作用是在生成 SQL 时,根据传入的条件决定是否包含某段 SQL 片段。简单来说就是判断两个表达式是否相等,比如判断po.status != '' 是否相等,来决定某个 SQL 语句是否会被执行或包含。
继续跟踪进去就会发现在判断两个表达式是否相等是用OGNL表达式来进行判断的
继续跟踪getValue方法
继续跟踪就会来到了 node.getValue方法,继续跟踪进去。
继续跟踪进去
最终来到了getValueBody的方法,点进去
最后来到了这里,这里只要是用来获取两个表达式的值的,v1获取的是第一个表达式,v2是获取到的是第二个表达式,比如po.status != '' ,在上述例子中,status传入的是0,所以v1获取到的是0,而v2是''。
所以v1 = 0;v2 = " ;
进入OgnlOps.equal(v1,v2)方法,由于v1不等于null,所以继续进入IsEqual(v1,v2)方法
因为Object1 不等于Object2 并且Object1显然不是空也不是数组,所以会进入最下面的else方法
也就是来到了最后进入到了compareWithConversion(object1, object2)这个方法,是一个内部方法,用于进行类型转换后再比较,确保不同类型(如 int 和 double)的数值也能正确比较。问题就是出现在了这里,我们进入这个方法里面继续观察
关键来了,进入到这个方法内部,就会发现有getNumericType(v1)和getNumericType(v2)这两个方法,这两个方法主要是用来匹配类型的,根据传入的对象类型返回一个对应的数值类型代码。它通过判断对象的类类型来给出不同类型的整数标识。点击进入一探究竟。
这是方法内部的完整代码,在这个代码中不难发现,getNumericType 方法中并没有针对 String 类型的处理。它主要是用来处理数值类型的比较,如 Integer、Double、Long 等。如果传入的对象是 String 类型,getNumericType 方法会返回默认的值 10,表示这是一个未被特别处理的类型。
由于里面没有针对string类型的处理,所以v1 = 0 返回了 4 ,而v2 = ’ ‘ 返回了10
public static int getNumericType(Object value) {
if (value != null) {
Class c = value.getClass();
if (c == Integer.class) {
return 4;
}
if (c == Double.class) {
return 8;
}
if (c == Boolean.class) {
return 0;
}
if (c == Byte.class) {
return 1;
}
if (c == Character.class) {
return 2;
}
if (c == Short.class) {
return 3;
}
if (c == Long.class) {
return 5;
}
if (c == Float.class) {
return 7;
}
if (c == BigInteger.class) {
return 6;
}
if (c == BigDecimal.class) {
return 9;
}
}
return 10;
}
而getNumericType(t1, t2, true)方法是用于计算 t1 和 t2 之间的 "最通用" 数值类型,以确保在比较 v1 和 v2 时两者可以在相同的基础上进行比较。例如: 如果 t1 是 Integer 而 t2 是 Double,它可能会返回一个更高的数值类型(如 Double),以便在比较时将 Integer 转换为 Double。 它确保不同类型的数值可以进行相对正确的比较。
在这个例子中t1 是 4(表示 Integer),t2 是 10(表示 String 或其他非数值类型)。 该方法将根据 t1 和 t2 的类型决定两者是否可以进行数值比较。如果其中一个是非数值类型(如 String),这将导致进入默认的 longValue 或 doubleValue 比较逻辑。
所以它会返回10,进入到switch中,匹配到了case10,跳出switch语句,然后来到了最关键的部分
这三段代码是整个流程的关键,由于v1 和 v2其中存在一个是非数值类型(如 String),这将导致进入默认的 longValue 或 doubleValue 比较逻辑。而v2正是非数值类型,所以到llongValue 或 doubleValue 比较逻辑。
在这个逻辑中他会将v1和v2转换成double类型,
由于v1 = 0 所以转换成了 0.0
在往下会发现 "(空字符串) 也会转换成0.0
为什么空字符串也会转换成0.0呢?
进入方法内部观察,首先判断类型,由于v2是string类型的,最终会到最后的else内部中。
由于v2 = '' ,空字符串会判断s.length() == 0这时候明显是返回true,然后 '' 字符串对等的double值就是0.0,所以v2也转换成了0.0
所以到了最后进行比较
double dv1 = doubleValue(v1);
double dv2 = doubleValue(v2);
return dv1 == dv2 ? 0 : (dv1 < dv2 ? -1 : 1);
所以dv1 = 0.0,dv2 = 0.0
dv1明显等于dev2的,所以会返回0,回到外部方法,最终0 = 0 是成立的,返回了true
result = compareWithConversion(object1, object2) == 0;
所以说 po.status != '' 相当于 0.0 != 0.0 这明显是false,所以说条件不成立,因此status为0的时候就拼接不了AND语句,自然就发生了查正常状态的位置,反而还出现了停用状态位置的现象了 就发生了查询正常状态的位置出现停用状态位置的现象了。
<select id="findPositionByNameAndStatus" resultType="com.ruoyi.system.domain.position.Position">
<include refid="selectPositionVO"></include>
<where>
<if test="po.name != null and po.name != ''">
AND name LIKE CONCAT('%', #{po.name}, '%')
</if>
<if test="po.status != null and po.status != ''">
AND status = #{po.status}
</if>
AND tenant_id = #{po.tenantId} and is_delete = 0 order by sort asc
</where>
</select>
解决问题
方案一
最简单的就是当Integer类型时就不需要跟空字符串比较了,因为不是string类型,如果是string类型就需要了
<select id="findPositionByNameAndStatus" resultType="com.ruoyi.system.domain.position.Position">
<include refid="selectPositionVO"></include>
<where>
<if test="po.name != null and po.name != ''">
AND name LIKE CONCAT('%', #{po.name}, '%')
</if>
<if test="po.status != null">
AND status = #{po.status}
</if>
AND tenant_id = #{po.tenantId} and is_delete = 0 order by sort asc
</where>
</select>
方案二
因为status为0的时候会被解析成0.0,而空字符串也会被解析成0.0,所以如果非要比较空字符串,可以这么判断,让staus == 空字符串就好了,因为上面我们通过分析mybatis的源码,它们都会被转换成0.0,所以是相等的,会进入if条件里进行拼接Sql语句
<select id="findPositionByNameAndStatus" resultType="com.ruoyi.system.domain.position.Position">
<include refid="selectPositionVO"></include>
<where>
<if test="po.name != null and po.name != ''">
AND name LIKE CONCAT('%', #{po.name}, '%')
</if>
<if test="po.status != null and po.status == ''">
AND status = #{po.status}
</if>
AND tenant_id = #{po.tenantId} and is_delete = 0 order by sort asc
</where>
</select>
方案三
在比较空字符串的同时在增加一个条件就好了,考虑为0的情况
<select id="findPositionByNameAndStatus" resultType="com.ruoyi.system.domain.position.Position">
<include refid="selectPositionVO"></include>
<where>
<if test="po.name != null and po.name != ''">
AND name LIKE CONCAT('%', #{po.name}, '%')
</if>
<if test="po.status != null and (po.status != '' or po.status == 0)">
AND status = #{po.status}
</if>
AND tenant_id = #{po.tenantId} and is_delete = 0 order by sort asc
</where>
</select>
结果
通过这三种方案,当status为0时都能正确的拼接sql语句。
功能最终恢复正常
结语
通过深入探讨了动态 SQL 生成和数据比较的细节。了解了 DynamicSqlSource 和 IfSqlNode 的作用,以及在处理字符串和数字比较时可能遇到的问题。希望这些见解能帮助大家优化 SQL 查询并处理数据类型转换中遇到的问题。能够更好地理解如何优化和调试动态 SQL 查询,确保在实际应用中能够得到预期的结果。如果有进一步的问题,欢迎交流!