Mybatis踩坑记录:探究Mybatis源码为何当传入参数Integer类型为0时,if条件生效

目录

前言

 ​编辑问题背景

 深入源码 

解决问题

方案一

方案二

方案三

 结果

结语


前言

在 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 查询,确保在实际应用中能够得到预期的结果。如果有进一步的问题,欢迎交流!

  • 19
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值