很开心,在使用 MyBatis 的过程中我踩到一个坑

栏目: IT技术 · 发布时间: 4年前

内容简介:才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

持续输出原创文章,点击蓝字关注我吧

这是why技术的第 14 篇原创文章

在实际开发过程中我踩到了mybatis的一个坑,我觉得值得记录、分享一下。

先说说这个坑是什么吧。如果你踩过这个坑,并且知道具体的原因,那这篇文章可以加深你的印象。如果你没有踩过,那你可得好好看看,因为你总会遇到的。

具体如下: 在mybatis中的OgnlOps.equal(0,"")返回的是true。

很开心,在使用 MyBatis 的过程中我踩到一个坑

首先这里返回为true就违背了我们的常识,其次返回为true,会带来什么问题呢?

看完本文你就清楚了。

本文会按照 遇到问题 --> 分析问题 --> 解决问题 的行文思路,用 追踪源码 的方法,对这个问题进行剖析。

同时分享一下我是怎么用 逆向排查 的方法,通过Debug模式找到最关键的那一行源码,然后明白前因后果,最后解决这个问题的。

本文源码: mybatis 3.5.3版本

背景介绍,需求分析

先铺垫一下背景,模拟一个需求。

有一个订单表,表结构如下:

很开心,在使用 MyBatis 的过程中我踩到一个坑

为了简化问题,我们假设表里面只有两条数据:

很开心,在使用 MyBatis 的过程中我踩到一个坑

订单号为1234的订单状态为0【关闭

订单号为4321的订单状态为1【开启

已经开发好的功能是模糊查询订单名称,接口如下(图中应该是字符串的OrderName):

很开心,在使用 MyBatis 的过程中我踩到一个坑

其对应的mapper.xml是这样写的,功能正常:

很开心,在使用 MyBatis 的过程中我踩到一个坑

现在需要在已有功能上添加一个根据状态过滤订单的功能:

很开心,在使用 MyBatis 的过程中我踩到一个坑

假设某个页面有这样的一个下拉框,可以根据订单状态过滤订单数据。

当用户选择【已支付】时,后台接收到的是数字1,用Byte类型接收。

当用户选择【未支付】时,后台接收到的是数字0,用Byte类型接收。

准备开发

现在明确了需求,根据订单状态进行过滤。

很简单,最主要的修改地方就是对mapper.xml的修改,至于怎么从前端传到xml来我就不详细说明了,相信用过mybatis的朋友都知道。

先在接口上加一个入参orderName:

很开心,在使用 MyBatis 的过程中我踩到一个坑

然后改造一下对应的xml:

很开心,在使用 MyBatis 的过程中我踩到一个坑

改造点很简单,在xml文件里面ctrl+c一下原来的if标签,再ctrl+v出来改改里面的名字就好了。

开始自测,遇到问题

请做好单元测试,即使这个功能非常简单,显而易见,你信心十足,但是做好单元测试,是一个 程序员 应有的职业素养。

单元测试如下:分别传入状态0和1

很开心,在使用 MyBatis 的过程中我踩到一个坑

按照我们现在表里的数据,我们预期的结果是各自查询出一条数据。

很开心,在使用 MyBatis 的过程中我踩到一个坑

运行起来,我们一起看看执行结果:

很开心,在使用 MyBatis 的过程中我踩到一个坑

status=0,查询出来的条数 = 2

status=1,查询出来的条数 = 1

很开心,在使用 MyBatis 的过程中我踩到一个坑

这结果和我们预期的不符呀!什么情况?

当时我遇到这个问题的时候,我就知道事情不简单,其中必有蹊跷。

如果是两年前,我遇到问题肯定是立马面向搜索引擎编程。把遇到的问题一顿搜索,根据网友的建议,很快就很解决了。然而,也很快就忘记了。而且,遇到这个问题的时候,我当时是没有联网的。

不要急着去问搜索引擎。不要慌,要分析,冷静分析之后才有收获。

分析问题

分析的第一步其实很容易想到,我们先把 sql 打印出来,看看最终执行的sql是什么,就知道为什么返回的结果和预期不符了。

所以我们在application.properties里面加上这行配置:

logging.level.com.xxxx.xxxx.mapper = debug

注:上面的xxxx换成自己的mapper包的路径

很开心,在使用 MyBatis 的过程中我踩到一个坑

加上sql打印后,我们发现当status为0时,mybatis并没有给我们拼接where关键字。

到这里很自然的就能联想到下一步:为什么mybatis没有给我们拼接where关键字?

或者换一个问法:mybatis是 在哪里 通过上 什么逻辑 拼接sql的?

常规的方法是加断点进行追踪, 但是我想分享一个我当时排查的"骚"操作,定位问题非常快。那就是 逆向排查

逆向排查法

现在我们确定了是sql拼接的问题,我通过日志,也 拿到了完整的sql。

日志配置是这样的:

logging.pattern.console=%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%C{56}.%method:%L -%msg%n

打印出来的日志是这样的(截取了其中一部分):

很开心,在使用 MyBatis 的过程中我踩到一个坑

org.apache.ibatis.logging.jdbc.BaseJdbcLogger的143行 ,debug方法中打印了日志,这行日志就是我的突破口。

在这个地方,我整个sql都拿到了,如果往回走,就能很快的找到sql是在哪里产生的。

那我在BaseJdbcLogger的143行,打上断点,并运行起来。

通过idea的Debug模式,我们可以得到从程序运行开始,到断点处的 整个调用链路 。(如果下面的图片看不清楚,可以点开查看大图):

很开心,在使用 MyBatis 的过程中我踩到一个坑

通过调用链,往后走三步,我们可以看到sql是从boundSql中获取到的:

很开心,在使用 MyBatis 的过程中我踩到一个坑

那么boundSql是从哪里来的呢?我们继续往回走。

往回走11步,我们可以看到boundSql的获取过程:

很开心,在使用 MyBatis 的过程中我踩到一个坑

为了方便大家找到源码,我把对应的方法名称放在这里:org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

还记得我们开始的问题吗,我们不知道sql是在哪里通过什么逻辑拼接的。

而这就是前一部分的答案呀。sql就是通过 org.apache.ibatis.executor.CachingExecutor的81行 代码产生的:

BoundSql boundSql = ms.getBoundSql(parameterObject);

所以接下来我们只需要在这行代码的前面打上断点,我们就能知道后半部分问题的答案了,通过什么逻辑拼接而成?

如果在你不是十分熟悉mybatis的情况下,你通过Debug模式正向的找到这行代码,是需要花一点时间的,而我上面说的逆向排查,可以节约一大部分时间。

关键源码

后面就是常规的正向查找的过程了,最终你会定位到这个全文最关键的地方:

org.apache.ibatis.ognl.ASTNotEq#getValueBody

很开心,在使用 MyBatis 的过程中我踩到一个坑

为什么在mybatis中数字0和空字符串""比返回的是true呢?源码之下无秘密,继续往下Debug你会找到这个地方:

org.apache.ibatis.ognl.OgnlOps#doubleValue

很开心,在使用 MyBatis 的过程中我踩到一个坑

这里返回之后,真正的对比是在这里:

很开心,在使用 MyBatis 的过程中我踩到一个坑

v1和v2最终都变成了0.0。所以返回了true。

由于OgnlOps.equal(0,"")返回为true,所以整个表达式【 OgnlOps.equal(0,"") ? Boolean.FALSE : Boolean.TRUE 返回的是FALSE。

接下来,需要回答的就是这三个问题了:

v1=0是哪里来的?

v2=""是从哪里来的?

返回FALSE会带来什么问题?

很开心,在使用 MyBatis 的过程中我踩到一个坑

图中标号为一的地方,就是v1的值,这个0是我传入的查询条件。

图中标号为二的地方,就是v2的值,这个""的来源是我写在mapper.xml文件中if标签里面的表达式。

图中标号为三的地方,为false的原因就是这个表达式 【OgnlOps.equal(0,"") ?Boolean.FALSE : Boolean.TRUE】返回的是false 。返回为false了,就不会进入下面的代码: contents.apply(context)

而这行代码,就是回答我们之前提出问题的后半部分,mybatis通过什么逻辑拼接sql?

很开心,在使用 MyBatis 的过程中我踩到一个坑

就是解析我们写在mapper.xml中的if标签中的test条件,如果满足条件,返回为true则拼接条件里面的内容,即sql。

由于这里的if标签是这样的:

其中orderStatus!=null返回为true,orderStatus !=''返回为false,所以整个表达式返回为false,则不拼接这个if标签里面的sql。

至此,我们结合源码,对于为什么会出现问题分析完毕。

解决问题

其实问题分析完了,一种解决方法也就呼之欲出,我们只需要把mapper.xml文件中的if标签修改为这样即可:

很开心,在使用 MyBatis 的过程中我踩到一个坑

或者改成这样:

很开心,在使用 MyBatis 的过程中我踩到一个坑

再看看执行结果:

很开心,在使用 MyBatis 的过程中我踩到一个坑

这样就和我们预期的结果一致了。

但是,你再回过头的想一想,我最开始的改造mapper.xml是怎么操作的:

改造点很简单,在xml文件里面ctrl+c一下原来的if标签,再ctrl+v出来改改里面的名字就好了。

是的,我无脑的使用了CV大法。导致我在欢声笑语中写出了bug。我orderStatus传入的类型是一个Byte,和""做判断有任何意义吗?

但是我也感谢这次无脑的CV,让我踩到了这个坑,并且研究清楚了。get到了新的知识点。

同时,我也感谢自己做了单元测试,不然测试同学测试的时候抛出这样的问题,我会觉得他不会用,他会觉得我是弱鸡。

最后说几句

在解决这个问题之后,我还是在网上查了一圈,发现也有人遇到了这样的问题,但是我点开搜索出来的第一篇就是一个错误的描述,他说在mybatis中会把0当做null来处理?哥们你看源码了吗?或者说我们说的不是一回事?

很开心,在使用 MyBatis 的过程中我踩到一个坑

然后还有其他的大量文章都只是扔给你一个解决方法,并没有写为什么这样写就可以解决这个问题。而这样的搜索结果在我看来是不完美的,因为很难留下深刻的印象,导致你或者你同事再次碰见这个问题的时候你会说:哦,这个问题呀,我之前碰见过。怎么解决的,我给忘了。

你这不废话吗?

我之前在《 面试了15位来自211/985院校的2020届研究生之后的思考 》这篇文章中写到一段话,用在这里也很合适:

很开心,在使用 MyBatis 的过程中我踩到一个坑

后来我把这个问题分享在群里之后,群里一个朋友也给我分享了一篇文章,肥朝大佬写的 《还有这种操作?浅析为什么要看源码》 。文中给出了另一种解决方案,有理有据,简明扼要,是一篇很好的文章,大家可以看看。

很开心,在使用 MyBatis 的过程中我踩到一个坑

尾声

文章写到这里也就接近尾声了。如果你能在这篇文章中get到这个知识点,或者当你碰到这个问题的时候能想起这篇文章,这就是对这篇文章最大的赞赏,文章价值的最高体现。

我更加希望的是,当你碰到这个问题,自己分析完了,在网上查询的时候看到了我的这篇文章。因为自己分析出来的,永远是印象最深刻的,其他的文章只是起点缀作用。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

如果你觉得文章还不错,你的点赞、留言、转发、分享、赞赏就是对我最大的鼓励。

感谢您的阅读,感谢您的关注。

以上。

很开心,在使用 MyBatis 的过程中我踩到一个坑

持续输出

长按识别关注

往期精彩

原创不易,点个赞吧

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。


以上所述就是小编给大家介绍的《很开心,在使用 MyBatis 的过程中我踩到一个坑》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Scrum敏捷软件开发

Scrum敏捷软件开发

Mike Cohn / 廖靖斌、吕梁岳、陈争云、阳陆育 / 清华大学出版社 / 2010-11 / 69.00元

《Scrum敏捷软件开发》是敏捷联盟及Scrum联盟创始人之一、敏捷估算及计划的鼻祖Mike Cohn三大经典著作中影响最为深厚的扛鼎之作,也是全球敏捷社区中获得广泛肯定的企业敏捷转型权威参考。作者花四年时间,把自己近十五年的敏捷实践经验,特别是近四年中针对各种敏捷转型企业的咨询和指导工作,并结合旁征博引的方式,从更高的思想层次对敏捷与Scrum多年来的经验和教训进行深入而前面的梳理和总结,最终集......一起来看看 《Scrum敏捷软件开发》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具