實(shí)戰(zhàn)系列:(二)一條被Mybatis誤導(dǎo)的sql語句

寫在前面

? ? ? ?網(wǎng)上很多的文章都是教科書式的說教磨德,缺乏實(shí)用價(jià)值瑰妄。這也是筆者想寫此系列文章的初衷轧铁,希望把實(shí)際工作的實(shí)戰(zhàn)經(jīng)驗(yàn)分享給大家珍德,幫助大家解決實(shí)際問題蚤告。后續(xù)的一系列文章都是筆者在實(shí)際工作遇到的問題努酸,比較具有代表性,從實(shí)戰(zhàn)的角度進(jìn)行分析總結(jié)杜恰,希望能夠給大家?guī)韼椭?br> 關(guān)鍵字:Spring Boot获诈、Mysql、Mybatis
:出于保密的目的心褐,對文章中出現(xiàn)的真實(shí)名稱(包括類名舔涎、方法名、數(shù)據(jù)庫名逗爹、表名亡嫌、字段名等)進(jìn)行了替換。

一、問題背景

? ? ? ?因?yàn)檫@個(gè)問題發(fā)生的場景是比較容易描述的挟冠,所以整個(gè)項(xiàng)目背景就不再贅述了于购。項(xiàng)目中使用的技術(shù)基本就是Spring Boot框架、Mysql數(shù)據(jù)庫知染、Mybatis作為OR Mapping工具肋僧,開發(fā)工具使用IntelliJ IDEA。
? ? ? ?這是一個(gè)多人合作的項(xiàng)目控淡,其中一人負(fù)責(zé)數(shù)據(jù)庫基本操作CRUD的實(shí)現(xiàn)嫌吠,其中有一個(gè)表table1中,有一個(gè)批量更新狀態(tài)(字段名為status)的操作逸寓,對應(yīng)的方法為Table1ServiceImpl實(shí)現(xiàn)類中的batchUpdateTable1Status(String ids, String status)。發(fā)現(xiàn)的問題是這個(gè)批量更新狀態(tài)的方法在做批量更新時(shí)(傳入多個(gè)id)并不生效覆山,但在做單條更新時(shí)是正常的竹伸。

二、問題定位

先把相關(guān)的代碼貼出來:
Table1Mapper.xml文件

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

Table1Dao.java文件

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

    // 其他內(nèi)容略…
}

Table1Service.java文件

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

    // 其他內(nèi)容略…
}

Table1ServiceImpl.java文件

public class Table1ServiceImpl {

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

    // 其他內(nèi)容略…
}

? ? ? ?看起來似乎一切都很正常簇宽,殊不知卻隱藏一個(gè)大大的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變量中只有一個(gè)id時(shí),更新成功魏割。如果needToUpdate變量中超過一個(gè)id時(shí)譬嚣,則更新不成功,也沒有任何報(bào)錯(cuò)钞它。沒有報(bào)錯(cuò)拜银,也沒有執(zhí)行成功,真是奇了怪了遭垛!把Mybatis的sql語句打印出來看看尼桶,到底問題在哪里?這也不難锯仪,修改項(xiàng)目的配置文件(我們使用的是yml文件)如下泵督,設(shè)置logger.level.com.ibatis=DEBUG,這樣就可以輸出sql信息了庶喜。

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

? ? ? ?又把代碼執(zhí)行一遍小腊,看看輸出的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 (?)久窟,兩個(gè)參數(shù)為1和以逗號分隔的幾個(gè)id值秩冈,分別替換語句模版中的兩個(gè)?的位置斥扛,最后執(zhí)行的語句應(yīng)該是這個(gè)樣子的吧:

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

? ? ? ?拿著這個(gè)自己攢的sql語句到Navicat里面手動執(zhí)行一下漩仙,更新成功!覺得更加奇怪了,為什么程序里面執(zhí)行不成功呢队他?
? ? ? ?手動生成這條sql語句的時(shí)候是想當(dāng)然了卷仑,此外,Mybatis這樣的輸出方式也存在一定的誤導(dǎo)性麸折。靜下心來仔細(xì)一想锡凝,知道問題出在什么地方了。其實(shí)是傳參的方式用錯(cuò)了垢啼,實(shí)際上Mybatis有兩種傳參的方式窜锯,一種是#{},另一種是${}芭析。#{}傳入的參數(shù)在sql中顯示為字符串锚扎;${}傳入的參數(shù)在sqL中直接顯示為傳入的值。更加明確一點(diǎn)馁启,#{}傳入值驾孔,sql解析時(shí),參數(shù)是帶引號的惯疙;而${}傳入值翠勉,sql解析時(shí),參數(shù)是不帶引號的霉颠。所以改成如下的sql語句对碌,則能夠?qū)崿F(xiàn)批量更新的效果。

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

? ? ? ?到此問題已經(jīng)得到解決蒿偎,但是作為一個(gè)合格的碼農(nóng)還是得保持點(diǎn)好奇心的朽们。為什么Mybatis就不能把最終執(zhí)行的完整sql語句打印出來呢?那樣不更容易幫助開發(fā)者定位錯(cuò)誤嗎诉位!給Mybatis一個(gè)差評华坦!

三、擴(kuò)展分析

? ? ? ?我們認(rèn)為最終生成的sql語句是這樣的:

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

而實(shí)際上是這樣的(因?yàn)槭褂昧?{}這種傳參的方式):

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

? ? ? ?因?yàn)檫@兩個(gè)參數(shù)都是String類型不从,正如mybatis打印出的log所描述的一樣:

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

? ? ? ?總有一種被誤導(dǎo)的感覺惜姐,如果Mybatis能夠輸出最終執(zhí)行的sql的話,可能更有助于發(fā)現(xiàn)問題椿息。嘗試把logging.level的值改成trace也沒有輸出最終的sql語句歹袁,和debug的輸出沒什么兩樣∏抻牛看來這條路是走不通了条舔,得想其他辦法了。這個(gè)也難不倒一個(gè)老碼農(nóng)乏矾,先把Mybatis的源代碼走馬觀花的過一遍孟抗,也許會有些啟發(fā)迁杨。
? ? ? ?首先發(fā)現(xiàn)了SqlSource這個(gè)接口,這個(gè)接口只有一個(gè)getBoundSql(Object parameterObject)方法凄硼,返回一個(gè)BoundSql對象铅协。一個(gè)BoundSql對象,代表了一次sql語句的實(shí)際執(zhí)行摊沉,而SqlSource對象的責(zé)任狐史,就是根據(jù)傳入的參數(shù)對象,動態(tài)計(jì)算出這個(gè)BoundSql说墨。這個(gè)BoundSql里面存放的就是在console里面輸出的如下部分骏全,還不是最終的執(zhí)行sql。

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

? ? ? ?繼續(xù)探索尼斧,又發(fā)現(xiàn)了org.apache.ibatis.mapping.MappedStatement這個(gè)類姜贡。MappedStatement類在Mybatis框架中用于表示XML文件中一個(gè)sql語句節(jié)點(diǎn),即一個(gè)<select />棺棵、<update />或者<insert />標(biāo)簽楼咳。Mybatis框架在初始化階段會對XML配置文件進(jìn)行讀取,將其中的sql語句節(jié)點(diǎn)對象化為一個(gè)個(gè)MappedStatement對象律秃。猜想Mybatis最終執(zhí)行sql的時(shí)候肯定要依靠這個(gè)類的爬橡,大體上應(yīng)該可以從這個(gè)類下手了治唤。
? ? ? ?在這個(gè)方法處(table1Service.batchUpdateTable1Status)加斷點(diǎn)棒动,step into進(jìn)去,一直跟蹤到Mybatis的代碼部分宾添。先到DefaultSqlSession的update方法處船惨,這個(gè)方法會首先獲取MappedStatement,然后再執(zhí)行這個(gè)MappedStatement缕陕,當(dāng)執(zhí)行到完
MappedStatement ms = this.configuration.getMappedStatement(statement)語句后粱锐,居然直接返回了,并沒有進(jìn)入到下一條語句扛邑。嘗試了強(qiáng)制進(jìn)入(force step into)也不沒有用怜浅,如果那位大俠有更好的辦法進(jìn)入到this.executor.update方法的話,請賜教蔬崩!


圖1恶座、此處無法debug進(jìn)入

? ? ? ?既然目前還找不到好的辦法直接進(jìn)入this.executor.update方法,那就用我的土辦法暴力闖入了沥阳。選中update方法跨琳,右鍵go to->declaration,原來是Executor接口的方法桐罕,再點(diǎn)擊代碼行左側(cè)的向下箭頭(?)處會有兩個(gè)實(shí)現(xiàn)類脉让,如下圖2所示:


圖2桂敛、Executor接口的實(shí)現(xiàn)類

? ? ? ?如果你不確定會執(zhí)行哪個(gè)實(shí)現(xiàn)方法,大不了都跳轉(zhuǎn)過去加上斷點(diǎn)溅潜。這里插一句术唬,大贊一下IDEA,實(shí)在是太好用了伟恶,簡直就是開發(fā)神器碴开,如果你還在使用Eclipse的話,勸你趕緊放棄Eclipse博秫,轉(zhuǎn)投IDEA的懷抱潦牛,保你不后悔!
? ? ? ?加上斷點(diǎn)后挡育,再繼續(xù)執(zhí)行的話巴碗,便順利進(jìn)入到了Executor.update方法的實(shí)現(xiàn)類中了。
? ? ? ?最后跟蹤到SimpleExecutor的doUpdate方法中即寒,就是這里了橡淆,在這里就要執(zhí)行最終的sql嘍!猜測這條sql應(yīng)該在stmt(Statement)這個(gè)變量里面母赵,展開stmt變量逸爵,再展開h,里面有個(gè)statement變量凹嘲。
圖3师倔、doUpdate方法

? ? ? ?點(diǎn)擊statement變量右邊的View就可以查看其全部內(nèi)容:


圖4、最終的執(zhí)行sql

? ? ? ?到這里就豁然開朗了周蹭,跟前面預(yù)想的完全吻合趋艘。這里就能看到完整的最終要執(zhí)行的sql語句了。所以問題的根本還是出在sql語句的寫法上凶朗。

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

? ? ? ?這里傳參的用法有問題瓷胧,不能用#{}這種方式,這種方式中字符串類型的變量會帶著引號替換的棚愤。當(dāng)然ids只有一個(gè)值時(shí)更新是沒問題的搓萧,當(dāng)ids有多個(gè)值時(shí),既然沒有語法錯(cuò)誤宛畦,Mybatis就正常執(zhí)行了瘸洛,也不會有任何錯(cuò)誤。只是邏輯上有問題而已刃永,多個(gè)id以逗號連接起來的一個(gè)值在數(shù)據(jù)庫中是肯定不存在的货矮,所以不能完成批量更新的操作。
? ? ? ?改為${}方式的傳遞參數(shù)后斯够,Mybatis中可以看到如下的sql語句囚玫。

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

? ? ? ?盡管上面這條語句能夠執(zhí)行成功喧锦,實(shí)際上還是存在一些問題的,因?yàn)閕d字段在數(shù)據(jù)庫中是varchar類型抓督,以上這條語句mysql會做隱式自動轉(zhuǎn)換燃少,故能執(zhí)行成功,但會存在效率問題铃在,關(guān)于這點(diǎn)將會在后續(xù)文章中再詳細(xì)討論阵具。

? ? ? ?實(shí)際上,下面這條語句才是正規(guī)的方式定铜。

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

? ? ? ?為了達(dá)到以上目的阳液,可以在Java代碼中進(jìn)行完善,在Java代碼中構(gòu)造參數(shù)時(shí)給每個(gè)id前后加上單引號('')揣炕。

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

四帘皿、總結(jié)

? ? ? ?技術(shù)來不得半點(diǎn)馬虎,憑空想象是沒有任何根據(jù)的畸陡。所以想真正的提高鹰溜,還是得有打破砂鍋問到底的勁頭才行!
? ? ? ?另外丁恭,基礎(chǔ)一定得扎實(shí)曹动,還有就是實(shí)踐性的學(xué)科,必須踩過坑才能更加了解問題的本質(zhì)牲览,紙上得來終覺淺澳钩隆!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末竭恬,一起剝皮案震驚了整個(gè)濱河市跛蛋,隨后出現(xiàn)的幾起案子熬的,更是在濱河造成了極大的恐慌痊硕,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件押框,死亡現(xiàn)場離奇詭異岔绸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)橡伞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進(jìn)店門盒揉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人兑徘,你說我怎么就攤上這事刚盈。” “怎么了挂脑?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵藕漱,是天一觀的道長欲侮。 經(jīng)常有香客問我,道長肋联,這世上最難降的妖魔是什么威蕉? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮橄仍,結(jié)果婚禮上韧涨,老公的妹妹穿的比我還像新娘。我一直安慰自己侮繁,他們只是感情好虑粥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宪哩,像睡著了一般舀奶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斋射,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天育勺,我揣著相機(jī)與錄音,去河邊找鬼罗岖。 笑死涧至,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桑包。 我是一名探鬼主播南蓬,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哑了!你這毒婦竟也來了赘方?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤弱左,失蹤者是張志新(化名)和其女友劉穎窄陡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拆火,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡跳夭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了们镜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片币叹。...
    茶點(diǎn)故事閱讀 39,769評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖模狭,靈堂內(nèi)的尸體忽然破棺而出颈抚,到底是詐尸還是另有隱情,我是刑警寧澤嚼鹉,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布贩汉,位于F島的核電站九妈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雾鬼。R本人自食惡果不足惜萌朱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望策菜。 院中可真熱鬧晶疼,春花似錦、人聲如沸又憨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蠢莺。三九已至寒匙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躏将,已是汗流浹背锄弱。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祸憋,地道東北人会宪。 一個(gè)月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像蚯窥,于是被迫代替她去往敵國和親掸鹅。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評論 2 354