实战系列:(二)一条被Mybatis误导的sql语句

写在前面

       网上很多的文章都是教科书式的说教,缺乏实用价值。这也是笔者想写此系列文章的初衷,希望把实际工作的实战经验分享给大家,帮助大家解决实际问题。后续的一系列文章都是笔者在实际工作遇到的问题,比较具有代表性,从实战的角度进行分析总结,希望能够给大家带来帮助。
关键字:Spring Boot、Mysql、Mybatis
:出于保密的目的,对文章中出现的真实名称(包括类名、方法名、数据库名、表名、字段名等)进行了替换。

一、问题背景

       因为这个问题发生的场景是比较容易描述的,所以整个项目背景就不再赘述了。项目中使用的技术基本就是Spring Boot框架、Mysql数据库、Mybatis作为OR Mapping工具,开发工具使用IntelliJ IDEA。
       这是一个多人合作的项目,其中一人负责数据库基本操作CRUD的实现,其中有一个表table1中,有一个批量更新状态(字段名为status)的操作,对应的方法为Table1ServiceImpl实现类中的batchUpdateTable1Status(String ids, String status)。发现的问题是这个批量更新状态的方法在做批量更新时(传入多个id)并不生效,但在做单条更新时是正常的。

二、问题定位

先把相关的代码贴出来:
Table1Mapper.xml文件

<update id="batchUpdateTable1Status" >
  update table1 set status = #{status}, update_time = NOW()
  where id in (#{ids})
</update>
其他内容略…

Table1Dao.java文件

public interface Table1Dao {
    Integer batchUpdateTable1Status(@Param("ids") String ids, @Param("status") String status);

    // 其他内容略…
}

Table1Service.java文件

public interface Table1Service {
    
    Integer batchUpdateTable1Status(String ids, String status);

    // 其他内容略…
}

Table1ServiceImpl.java文件

public class Table1ServiceImpl {

    @Autowired
    private Table1Dao table1Dao;
    
    @Override
    public Integer batchUpdateTable1Status(String ids, String status) {
        return table1Dao.batchUpdateTable1Status(ids,status);
    }

    // 其他内容略…
}

       看起来似乎一切都很正常,殊不知却隐藏一个大大的bug。程序中的用法如下:

@Autowired
private Table1Service table1Service;


List<String> needToUpdate = new ArrayList<>();
for(String id : existsIds) {
    needToUpdate.add(id);
}

String updateIds = StringUtils.join(needToUpdate, ",");
table1Service.batchUpdateTable1Status(updateIds, "1");

       如果needToUpdate变量中只有一个id时,更新成功。如果needToUpdate变量中超过一个id时,则更新不成功,也没有任何报错。没有报错,也没有执行成功,真是奇了怪了!把Mybatis的sql语句打印出来看看,到底问题在哪里?这也不难,修改项目的配置文件(我们使用的是yml文件)如下,设置logger.level.com.ibatis=DEBUG,这样就可以输出sql信息了。

#log配置
logging:
  path: /tmp/logs
  config: classpath:logback-spring.xml
  level:
    com.ibatis: DEBUG

       又把代码执行一遍,看看输出的sql信息是什么。

JDBC Connection [HikariProxyConnection@1495740989 wrapping com.mysql.jdbc.JDBC4Connection@1cec0e3d] will not be managed by Spring
==>  Preparing: update table1 set status = ?, update_time = NOW() where id in (?) 
==> Parameters: 1(String), 19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316(String)
2019-07-04 23:57:51.269  WARN 1376 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=8m18s941ms)

       语句模版是update table1 set status = ?, update_time = NOW() where id in (?),两个参数为1和以逗号分隔的几个id值,分别替换语句模版中的两个?的位置,最后执行的语句应该是这个样子的吧:

update table1 set status = 1, update_time = NOW() where id in (19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316)

       拿着这个自己攒的sql语句到Navicat里面手动执行一下,更新成功!觉得更加奇怪了,为什么程序里面执行不成功呢?
       手动生成这条sql语句的时候是想当然了,此外,Mybatis这样的输出方式也存在一定的误导性。静下心来仔细一想,知道问题出在什么地方了。其实是传参的方式用错了,实际上Mybatis有两种传参的方式,一种是#{},另一种是${}。#{}传入的参数在sql中显示为字符串;${}传入的参数在sqL中直接显示为传入的值。更加明确一点,#{}传入值,sql解析时,参数是带引号的;而${}传入值,sql解析时,参数是不带引号的。所以改成如下的sql语句,则能够实现批量更新的效果。

update table1 set status = #{status}, update_time = NOW() where id in (${ids})

       到此问题已经得到解决,但是作为一个合格的码农还是得保持点好奇心的。为什么Mybatis就不能把最终执行的完整sql语句打印出来呢?那样不更容易帮助开发者定位错误吗!给Mybatis一个差评!

三、扩展分析

       我们认为最终生成的sql语句是这样的:

update table1 set status = 1, update_time = NOW() where id in (19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316)

而实际上是这样的(因为使用了#{}这种传参的方式):

update table1 set status = ‘1’, update_time = NOW() where id in (‘19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316’)

       因为这两个参数都是String类型,正如mybatis打印出的log所描述的一样:

==> Parameters: 1(String), 19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316(String)

       总有一种被误导的感觉,如果Mybatis能够输出最终执行的sql的话,可能更有助于发现问题。尝试把logging.level的值改成trace也没有输出最终的sql语句,和debug的输出没什么两样。看来这条路是走不通了,得想其他办法了。这个也难不倒一个老码农,先把Mybatis的源代码走马观花的过一遍,也许会有些启发。
       首先发现了SqlSource这个接口,这个接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql。这个BoundSql里面存放的就是在console里面输出的如下部分,还不是最终的执行sql。

==>  Preparing: update table1 set status = ?, update_time = NOW() where id in (?) 
==> Parameters: 1(String), 19052319311696852799,19052915401102966297,19052917301921635822,19053015191594083316(String)

       继续探索,又发现了org.apache.ibatis.mapping.MappedStatement这个类。MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点,即一个<select />、<update />或者<insert />标签。Mybatis框架在初始化阶段会对XML配置文件进行读取,将其中的sql语句节点对象化为一个个MappedStatement对象。猜想Mybatis最终执行sql的时候肯定要依靠这个类的,大体上应该可以从这个类下手了。
       在这个方法处(table1Service.batchUpdateTable1Status)加断点,step into进去,一直跟踪到Mybatis的代码部分。先到DefaultSqlSession的update方法处,这个方法会首先获取MappedStatement,然后再执行这个MappedStatement,当执行到完
MappedStatement ms = this.configuration.getMappedStatement(statement)语句后,居然直接返回了,并没有进入到下一条语句。尝试了强制进入(force step into)也不没有用,如果那位大侠有更好的办法进入到this.executor.update方法的话,请赐教!


图1、此处无法debug进入

       既然目前还找不到好的办法直接进入this.executor.update方法,那就用我的土办法暴力闯入了。选中update方法,右键go to->declaration,原来是Executor接口的方法,再点击代码行左侧的向下箭头(⬇)处会有两个实现类,如下图2所示:


图2、Executor接口的实现类

       如果你不确定会执行哪个实现方法,大不了都跳转过去加上断点。这里插一句,大赞一下IDEA,实在是太好用了,简直就是开发神器,如果你还在使用Eclipse的话,劝你赶紧放弃Eclipse,转投IDEA的怀抱,保你不后悔!
       加上断点后,再继续执行的话,便顺利进入到了Executor.update方法的实现类中了。
       最后跟踪到SimpleExecutor的doUpdate方法中,就是这里了,在这里就要执行最终的sql喽!猜测这条sql应该在stmt(Statement)这个变量里面,展开stmt变量,再展开h,里面有个statement变量。
图3、doUpdate方法

       点击statement变量右边的View就可以查看其全部内容:


图4、最终的执行sql

       到这里就豁然开朗了,跟前面预想的完全吻合。这里就能看到完整的最终要执行的sql语句了。所以问题的根本还是出在sql语句的写法上。

update table1 set status = #{status}, update_time = NOW() where id in (#{ids})

       这里传参的用法有问题,不能用#{}这种方式,这种方式中字符串类型的变量会带着引号替换的。当然ids只有一个值时更新是没问题的,当ids有多个值时,既然没有语法错误,Mybatis就正常执行了,也不会有任何错误。只是逻辑上有问题而已,多个id以逗号连接起来的一个值在数据库中是肯定不存在的,所以不能完成批量更新的操作。
       改为${}方式的传递参数后,Mybatis中可以看到如下的sql语句。

HikariProxyPreparedStatement@1390780895 wrapping com.mysql.jdbc.JDBC4PreparedStatement@1994b6f0: update table1 set status = '-1', update_time = NOW()
      where id in (19053116010337350268,19052817391182722422,19052817391063053316)

       尽管上面这条语句能够执行成功,实际上还是存在一些问题的,因为id字段在数据库中是varchar类型,以上这条语句mysql会做隐式自动转换,故能执行成功,但会存在效率问题,关于这点将会在后续文章中再详细讨论。

       实际上,下面这条语句才是正规的方式。

HikariProxyPreparedStatement@1390780895 wrapping com.mysql.jdbc.JDBC4PreparedStatement@1994b6f0: update table1 set status = '-1', update_time = NOW()
      where id in ('19053116010337350268','19052817391182722422','19052817391063053316')

       为了达到以上目的,可以在Java代码中进行完善,在Java代码中构造参数时给每个id前后加上单引号('')。

needToUpdate.add("'" + id + "'");

四、总结

       技术来不得半点马虎,凭空想象是没有任何根据的。所以想真正的提高,还是得有打破砂锅问到底的劲头才行!
       另外,基础一定得扎实,还有就是实践性的学科,必须踩过坑才能更加了解问题的本质,纸上得来终觉浅啊!

                                                                             2019年7月6日 于北京通州家中

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352