聊聊 Mybatis 動態(tài) SQL,你學(xué)會了嗎?
圖片
1 什么是 Mybatis 動態(tài)SQL
如果你使用過 JDBC 或其它類似的框架,你應(yīng)該能理解根據(jù)不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。
Mybatis 借助功能強大 OGNL 表達式,可以根據(jù)參數(shù)條件,動態(tài)生成執(zhí)行 SQL 。
使用動態(tài) SQL 最常見情景是根據(jù)條件包含 where 子句的一部分。
圖片
這條語句提供了可選的查詢博客文章列表 ,如果不傳入 “title”,那么所有處于 “ACTIVE” 狀態(tài)的 博客都會返回。
如果傳入了 “title” 參數(shù),那么就會對 “title” 一列進行模糊查找并返回對應(yīng)的 BLOG 結(jié)果。
如果希望通過 “title” 和 “author” 兩個參數(shù)都可以搜索,只需要加入另一個條件即可,見下圖:
圖片
我們也可以使用 where 標簽,該標簽只會在子元素返回任何內(nèi)容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 標簽也會將它們?nèi)コ?/p>
圖片
Mybatis 還支持 choose (when, otherwise)、trim (where, set)、foreach 等其他的動態(tài)標簽,這里就不一一贅述了。
2 人生第一次線上OOM事故
我曾服務(wù)一家電商公司的用戶中心,用戶中心提供用戶注冊,查詢,修改等基礎(chǔ)功能 。
那個時候 Dubbo 等 RPC 框架并沒有開源,所有的服務(wù)都以 HTTP 接口形式提供,數(shù)據(jù)傳輸格式是 XML 。
因為寫接口非常費勁,所以為了接口復(fù)用,我寫了一個通用接口 getUserByConditions ,該接口支持通過 「用戶名」、「昵稱」、「手機號」、「用戶編號」這三個查詢用戶基本信息。
使用的是 ibatis (mybatis 的前身), SQLMap 見下圖 。
圖片
當構(gòu)建動態(tài) SQL 查詢時,條件通常會追加到 WHERE
子句后,而以 WHERE 1 = 1
開頭,可以輕松地使用 AND
追加其他條件。
用戶中心在上線后,竟然每隔三四個小時就發(fā)生了內(nèi)存溢出問題 ,經(jīng)過通過和 DBA 溝通,發(fā)現(xiàn)高頻次出現(xiàn)全表查用戶表,執(zhí)行 SQL 變成 :
圖片
查看日志后,發(fā)現(xiàn)前端傳遞的參數(shù)出現(xiàn)了空字符串,筆者在代碼中并沒有做參數(shù)校驗,所以才出現(xiàn)全表查詢 ,當時用戶表的數(shù)據(jù)是 1000多萬 ,頁面調(diào)用幾次,用戶中心服務(wù)就 OOM 了。
最終解決問題的方式很簡單,后端在接收參數(shù)時,做了參數(shù)校驗。
事故雖然解決了,但對我的影響一直延續(xù)至今。我仍然記得當時站在運維同學(xué)旁邊,不斷的看他調(diào)整 JVM 參數(shù) ,重啟服務(wù)的畫面,自己那個時候真的是羞愧難當,心中發(fā)誓:以后絕對不可以發(fā)生類似的事故。
對于動態(tài) SQL ,我的編程思維也經(jīng)歷了如下三個階段 :
- 前后端參數(shù)校驗
- 復(fù)用和專用要做平衡
- 防御性編程意識
3 前后端參數(shù)校驗
為了提升開發(fā)效率,我們?nèi)藶榈膶⑾到y(tǒng)分為前端、后端,分別由兩撥不同的人員開發(fā) ,經(jīng)常出現(xiàn)系統(tǒng)問題時,兩撥人都非常不服氣,相互指責。
要想系統(tǒng)健壯,前后端應(yīng)該同時做接口參數(shù)校驗 (后端必須做參數(shù)校驗),當大家都遵循這個規(guī)約時,出現(xiàn)系統(tǒng)問題的風險大大減少。
1)前端校驗
圖片
前端校驗,主要是為了提高用戶體驗,例如用戶輸入一個郵箱地址,要校驗這個郵箱地址是否合法,沒有必要發(fā)送到服務(wù)端進行校驗,直接在前端用 js 進行校驗即可。
但是大家需要明白的是,前端校驗無法代替后端校驗,前端校驗可以有效的提高用戶體驗,但是無法確保數(shù)據(jù)完整性,因為在 B/S 架構(gòu)中,用戶可以方便的拿到請求地址,然后直接發(fā)送請求,傳遞非法參數(shù)。
2)后端校驗
后端必須做參數(shù)校驗,后端必須做參數(shù)校驗,后端必須做參數(shù)校驗,重要的事情表達三次。
數(shù)據(jù)在網(wǎng)絡(luò)傳輸過程中有可能被篡改了,或者數(shù)據(jù)不滿足業(yè)務(wù)需求,假如不做后端參數(shù)校驗,則有可能導(dǎo)致系統(tǒng)異常,或者業(yè)務(wù)出現(xiàn)重大事故。
比如在 SpringBoot 項目中,我們可以使用 hibernate-validator 進行參數(shù)校驗 。
POST、PUT 請求一般會使用 requestBody 傳遞參數(shù),這種情況下,后端使用 DTO 對象進行接收。
只要給 DTO 對象加上 @Validated 注解就能實現(xiàn)自動參數(shù)校驗。比如,有一個保存 User 的接口,要求 userName 長度是 2-10,account 和 password 字段長度
是 6-20。
在 DTO 字段上聲明約束注解:
圖片
在方法參數(shù)上聲明校驗注解:
圖片
雖然,我們可以使用接口校驗,可以保證動態(tài) SQL 的參數(shù)正確,但是假如我們僅僅只是復(fù)用 SQLMap (Dao 方法)時,也有可能因為調(diào)用方傳遞參數(shù)錯誤,導(dǎo)致非預(yù)期的問題。
當然,我們也可以使用 Mybatis 攔截器從根本上來解決,但是我想這樣會加大系統(tǒng)的復(fù)雜度。于是,我思考了了另外一點:復(fù)用和專用要做平衡。
4 復(fù)用和專用要做平衡
我當時寫的那個接口 getUserByConditions ,是支持四種不同參數(shù)的查詢,同樣也是為了省時間,快點出活。
后來,隨著我工作經(jīng)驗的日益豐富,我的編程習慣也慢慢發(fā)生了改變,對于業(yè)務(wù)需求明確的場景,我更多的傾向于將通用接口拆分成專用接口。
比如 getUserByConditions 可以拆分成如下四個接口 ,
- 按照用戶 ID 查詢用戶信息
- 按照用戶昵稱查詢用戶信息
- 按照手機號查詢用戶信息
- 按照用戶名查詢用戶信息
比如按照用戶 ID 查詢用戶信息 , SQLMAP 就簡化為:
圖片
通過這樣的拆分,我們的接口設(shè)計更加細粒度,也更容易維護 , 同時也可以規(guī)避 where 1 =1 產(chǎn)生的不確定性(雖然我做了后端校驗,依然存在不確定性)。
有的同學(xué)會有疑問:假如拆分得太細,會不會增加我編寫接口和 SQLMap 的工作量 ?
筆者的思路是:定制自己的代碼生成器,將生成的 SQLMap 、Mapper 保證更細的顆粒度。
5 防御性編程意識
筆者剛?cè)胄械臅r候,只是機械性的完成任務(wù),并沒有思考代碼后面的資源占用,以及有沒有可能產(chǎn)生惡劣的影響。
隨著見識更多的系統(tǒng),學(xué)習開源項目,筆者慢慢培養(yǎng)了一種習慣:
- 這段代碼會占用多少系統(tǒng)資源
- 如何規(guī)避風險 ,做好預(yù)防性編程。
其實,這和玩游戲差不多 ,在玩游戲的時,我們經(jīng)常說一個詞,那就是意識。
圖片
上圖,后裔跟墨子在壓對面馬可蔡文姬,看到小地圖中路鎧跟小喬的視野,方向是往下路來的,這時候我們就得到了一個信息。
知道對面的人要來抓,或者是協(xié)防,這種情況我們只有兩個人,其他的隊友都不在,只能選擇避戰(zhàn),強打只會損失兩名“大將”。
通過小地圖的信息,并且想出應(yīng)對方法,就是叫做“猜測意識”。
編程也是一樣的,我們思考代碼可能產(chǎn)生的系統(tǒng)資源占用,以及可能存在的風險,并做好防御性編程,就是編程的意識。
6 寫到最后
人生第一次線上 OOM 事故,因我在使用 Mybatis 動態(tài) SQL 時,沒有做后端校驗而出現(xiàn),造成了比較壞的影響。
在后面的職業(yè)生涯里面,為了規(guī)避生產(chǎn)環(huán)境的事故,我試著打磨自己的編程思維,比如做好后端校驗、平衡好復(fù)用和專用接口、培養(yǎng)防御性編程的意識 。