角度新奇!第一次看到這樣使用MyBatis的,看得我一愣一愣的。

2023-08-22 06:02:20

你好呀,我是歪歪。

這期給大家分享一個讀者給我分享的一個關於 MyBatis 的「程式設計小技巧」,說真的,這騷操作,直接把我看得一愣一愣的。

我更情願叫它:坑你沒商量之埋雷大法。

Demo

為了讓你絲滑入戲,我還是先給你搞個 Demo。

因為要使用到 MyBatis 嘛,所以我們先搞兩個表。

一個表叫做 product 表,表結構非常簡單:

另一個表叫做 order_info 表,表結構也非常簡單:

看到這兩個表出現的時候,你就知道我的場景是啥了,肯定是賣貨嘛。

庫存減一,訂單加一。

大家再熟悉不過的場景了。

分分鐘能寫出這樣的虛擬碼:

public void saleProduct(){
    //更新庫存,庫存減一
    productMapper.updateProductCount();
    //儲存訂單資訊
    orderInfoMapper.saveOrderInfo();
}

當然了,這個虛擬碼你一眼就能看出問題:減庫存和儲存訂單應該是一個事務操作,所以應該把這兩個動作包裹在事務裡面。

於是我們的虛擬碼變成了這樣:

public void saleProduct() {
    //開啟事務
    begin;
    //更新庫存,庫存減一
    Boolean updateSuccess = productMapper.updateProductCount();
    //儲存訂單資訊
    orderInfoMapper.saveOrderInfo();
    if (updateSuccess) {
        //提交事務
        commit;
    } else {
        //回滾事務
        rollback
    }
}

當時讀者給我舉例的時候,完全是另外一個場景,和賣貨完全沒有任何關係。

讀者舉的例子大概是幾個表之間有關聯關係,如果一個表的某條資料被刪除了,另外幾個表裡面對應的資料也要刪除,還有一個表需要更新狀態。

為了更好的展示這個「程式設計小技巧」,我才把場景簡化到了前面提到的賣貨的樣子。

前面說的是虛擬碼。

現在我給你展示一下用「程式設計小技巧」寫出來的真實的程式碼。

首先是 controller 介面:

@GetMapping("/sale")
public void sale() {
    productMapper.selaProduct();
}

然後是這個 productMapper 的 selaProduct 介面:

是的,你沒有看錯,這就是一個 MyBatis 的 mapper 介面,接下來就直接到了 mapper.xml 檔案裡面:

這寫法,這小技巧,我都不打算問你騷不騷,我就問你見沒見過?

能用嗎?

歪師傅還是太年輕,見識不夠,在這之前從來沒見過在 mapper.xml 裡面能這樣去寫 sql 的。

不說見過,在我的小腦袋裡面,我是壓根就沒想過這樣去寫。所以看到這個寫法的第一反應是:這能行嗎?這不行吧?

於是,秉承著大膽假設小心求證的態度,寫了上面的 Demo。

專案啟動之後發起呼叫,控制檯直接報了錯:

看到這個報錯的時候,我下意識的覺得就是 MyBatis 不支援這樣的寫法,直接報錯了,這也符合我之前的認知。

但是,在讀者的指導下,他提醒我在資料庫連線的設定上加上這樣的設定:

allowMultiQueries=true

我的 Demo 啟動的時候,確實沒有加這個設定。但是看到這個設定的一瞬間,我開始覺得有點意思了。

因為我知道這個設定是幹嘛的。

見名知意嘛:allow Multi Queries,允許進行多個查詢。

最常用的場景就是用 foreach 標籤來進行批次插入或者更新的時候會用到這個設定。

在這個引數的加持下,前面 mapper.xml 裡面的寫的那個 sql,很有可能就能正常執行了。

因為加入這個設定之後,可以在一個資料庫連線中執行多個 sql 語句,而對於 MyBatis 或者 MySQL 的驅動來說,它並不區這「多個 sql」都是 insert 語句還是 update 語句,或者是混合著都有的語句。

我也去 MySQL 官網上查詢了這個設定的含義:

https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-connp-props-security.html#cj-conn-prop_allowMultiQueries

對於這個引數,官網上就一句話:

Allow the use of ";" to delimit multiple queries during one statement. This option does not affect the 'addBatch()' and 'executeBatch()' methods, which rely on 'rewriteBatchStatements' instead.
允許在一條語句中使用"; "分隔多個查詢。該選項不會影響 "addBatch() "和 "executeBatch() "方法,因為它們依賴於 "rewriteBatchStatements"。

在介紹 allowMultiQueries 的時候,還提到了一個 rewriteBatchStatements 引數。

關於這個引數是幹啥的,我這裡就不展開描述了,我只能說這兩個玩意是一套組合拳,裡面也大有文章,如果你不知道,建議你去了解一下。

就當是課後習題了。

我們還是先跟著主幹走。

當我在資料庫連線上追加設定 allowMultiQueries=true 之後,重啟了服務。

再次發起呼叫。

為了表示我的震驚,我給你搞個動圖:

庫存減一,訂單加一,方法執行成功了。

還真 TM 能用,你說這事搞的,實屬是開了眼了。

這波漲知識了,屬於未曾設想過的道路。

埋雷

千萬別這樣寫!

聽歪師傅一句勸,千萬別這樣寫!

首先這樣的寫法就不符合絕大部分程式設計師的認知。

試問誰能想到最後的 mapper.xml 裡面,並不只是簡簡單單的 sql,裡面居然還埋在一坨業務邏輯呢?

關鍵是這樣寫也埋雷啊。

舉個簡單的例子,這樣的寫法,完全沒有考慮庫存是否足夠的情況:

比如,當前庫存沒有了,按照這樣的寫法,還是會在 order_info 表裡面插入一條資料。

超賣了,朋友。

只有 commit,沒有考慮回滾的情況。

而且這樣寫根本就完全不可能考慮超賣的情況,因為你拿不到扣減庫存的操作是否執行成功,從而無法判斷是需要 commit 還是 rollback。

什麼,你問我能不能寫儲存過程來判斷?

能,MyBatis 確實可以呼叫儲存過程。

首先,儲存過程還是得在 MySQL 裡面寫好,MyBatis 只是發起呼叫。

其次,趕緊打消你這個越走越遠的騷想法,老老實實的寫 Java 程式碼來解決這個問題,它不香嗎?

什麼,你又問我如果是不需要判斷前一條 sql 是否執行成功的場景呢?

比如我前面提到的讀者舉的例子,幾個表之間有關聯關係,如果一個表的某條資料被刪除了,另外幾個表裡面對應的資料也要刪除,還有一個表需要更新狀態。

大概是這樣的:

begin;
delete from table1 where user_id=xxx;
delete from table2 where user_id=xxx;
delete from table3 where user_id=xxx;
update table4 set user_status=1 where user_id=xxx;
commit;

和賣貨的場景不一樣的是,在這個場景下如果每個 sql 執行成功,則代表業務執行成功。

看起來,似乎沒什麼問題。

但是我問你一個問題:這一組 SQL 一定會走都 commit 嗎?

你好好想想?

肯定不一定嘛,保不齊執行的過程中出什麼么蛾子。

舉個最簡單的例子,表寫錯了:

在這個場景下,再次發起呼叫:

程式報錯說找不到這個表。

那麼請問:此時,訂單表是否應該有資料被插入?

出異常了,肯定不應該有資料插入。我看了資料庫,確實也沒有新資料插入。

看起來確實沒問題。

那麼再請問:在這種寫法的情況下,當前這個事務是被回滾了還是被提交了?

。。。

。。。

。。。

正確答案是被掛起了。

通過執行下面這個 SQL,我們可以獲取到當前事務列表:

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

通過查詢結果可以發現,在我們程式丟擲異常之後,當前事務還在 RUNNING 狀態:

而且,這個事務在服務重啟之前,將一直在 RUNNING 狀態,即被掛起了。

但僅從程式的角度看,丟擲異常,沒有資料,符合預期,沒有任何毛病。

埋雷了。

所以,聽歪師傅一句勸,千萬別這樣寫!

老老實實的寫大家都看得懂的 Java 程式碼,不要在 mapper.xml 裡面搞事情。

擴充套件

其實我覺得吧,前面都屬於卵用不大的知識點,因為大家一般都不會這樣去寫。

但是既然都寫到這裡了,場景也有了,我也給大家擴充套件一個稍微有點用的知識。

還是在賣貨的場景下。

訂單加一,庫存減一是這樣的。

begin;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪師傅''ipad pro頂配版');
update product set product_count=product_count-1 where id=1 and product_count>0;
commit;

而庫存減一,訂單加一是這樣的:

begin;
update product set product_count=product_count-1 where id=1 and product_count>0;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪師傅''ipad pro頂配版');
commit;

都是包裹在事務裡面,為了簡化程式碼,我們假設庫存非常夠用,先不考慮 rollback 的場景。

請問是「訂單加一,庫存減一」的效能好,還是「庫存減一,訂單加一」的效能好,還是說這二者沒有什麼區別?

首先,從執行結果上看,這二者確實是沒有什麼區別的,都能保證業務場景的正確性。

但是當你考慮效能的時候,肯定是「訂單加一,庫存減一」的效能更好。

如果你沒想明白的話,我給你一個簡單的提示:在業務正確的前提下,加鎖的程式碼越靠近解鎖的程式碼,是不是效能越好?

如果你還沒想明白的話,我再給你一個提示:庫存減一,它會加鎖嗎?你不管它是加表鎖、間隙鎖還是記錄鎖,我就問你它加不加鎖?

如果你還沒反應過來的話,說明你對於 MySQL 的加鎖機制掌握的有點薄弱,可以去加固一下。

我直接公佈答案了:

update product set product_count=product_count-1 where id=1 and product_count>0;

因為 where 條件中是 id=1,所以鎖是加在唯一索引上的,而且表中存在該記錄,所以只會對 id=1 這行記錄加鎖。

針對 id=1 這一個產品來說,如果它是一個熱點商品,我們採取「訂單加一,庫存減一」的寫法,效能會更高一點。

因為在加鎖頻率相同的情況下,解鎖越快的,效能越高。

上個圖你就明白了:

調換一個 SQL 的事兒,效能就上去了,我就問你舒不舒服?

最後,再說個不相關的:

我在文章最開始的地方給了這樣的一個圖片:

你不覺得彆扭嗎?

sela 是什麼鬼?

很明顯,這個地方是一個單純的拼寫錯誤,想要打出的單詞是 sale:

請問,當你在程式裡面看到這樣的拼寫的時候,你會怎麼辦?

如果是我,我會主動把 selaProduct 修改為 saleProduct,其他什麼都不會動。

這就是我在之前的文章中提到的一個編碼規則,童子軍軍規:

修改一個拼寫錯誤的方法名、變數名,在程式碼裡面也是一件很重要的小事。

這不是程式碼潔癖,這是基本的職業道德。

因為你也不想下一個接手你程式碼的人,因為看到一堆叫做「succeess、createTiem、lastUpdataBy、bussinessDate、proudectName」等等這些變數名而血壓上升,氣大傷身。