gorm.Clause()子句分析之ON DUPLICATE KEY UPDATE
一、背景介紹
最近看到一段程式碼,使用到了gorm的Clause()子句,大概如圖所示。之前由於沒用過Clause()子句,所以本文對Clause()子句先進行研究,然後分析sql語句。
二、Clause()子句
GORM 內部使用 SQL builder 生成 SQL。對於每個操作,GORM 都會建立一個*gorm.Statement
物件,所有的 GORM API 都是在為statement
新增/修改Clause
,最後,GORM 會根據這些 Clause 生成 SQL。例如,當通過First
進行查詢時,它會在Statement
中新增以下 Clause :
clause.Select{Columns: "*"} clause.From{Tables: clause.CurrentTable} clause.Limit{Limit: 1} clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, }
然後 GORM 在Query
callback 中構建最終的查詢 SQL,像這樣:
Statement.Build("SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR")
生成 SQL:
SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
您可以自定義Clause
並與 GORM 一起使用,這需要實現Interface介面,
2.1、子句構造器
不同的資料庫, Clause 可能會生成不同的 SQL,例如:
db.Offset(10).Limit(5).Find(&users) // SQL Server 會生成 // SELECT * FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY // MySQL 會生成 // SELECT * FROM `users` LIMIT 5 OFFSET 10
子句選項之所以支援 Clause,是因為 GORM 允許資料庫驅動程式通過註冊 Clause Builder 來取代預設值,GORM 定義了很多Clause,其中一些 Clause 提供了你可能會用到的選項,儘管很少會用到它們,但如果你發現 GORM API 與你的預期不符合。這可能可以很好地檢查它們,例如:
db.Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&user) // INSERT IGNORE INTO users (name,age...) VALUES ("jinzhu",18...);
三、ON DUPLICATE KEY UPDATE
針對本文開頭的程式碼,加上Debug(),打印出sql語句如下:
INSERT INTO `user_info` (`user_id`,`door_id`,`email`,`address`,`create_time`,`update_time`) VALUES (666,888,'[email protected]','北京市海淀區','2021-07-28 22:26:20.241','2021-07-28 22:26:20.241') ON DUPLICATE KEY UPDATE `email`=VALUES(`email`),`address`=VALUES(`address`),`update_time`=VALUES(`update_time`)
使用這條語句的原因,是為了更好的執行插入和更新,因為我們在插入一條語句時,表中可能已經存在了這條語句,我們想實現更新的功能,或者表中沒有這條語句,我們想實現插入的功能,而這條語句直接可以同時解決插入和更新的功能。
那麼這條語句是如何解釋呢,我們很容易理解前面的部分,就是一個簡單的插入語句,讓我們看下後面的部分ON DUPLICATE KEY UPDATE `email`=VALUES(`email`),`address`=VALUES(`address`),`update_time`=VALUES(`update_time`)我們看到後面是一個更新的操作,後面指定了更新的欄位,也就是說判斷出表中沒有這條資料,執行的前半部分,插入指定欄位得值,在判斷出表中有資料,則執行的的更新操作,更新後半部分指定的欄位的值。
那麼下一個問題出來了,我們是如何判斷出這條資料是存在的,又需要更新哪些欄位呢?
規則如下:
如果你插入的記錄導致UNIQUE索引重複,那麼就會認為該條記錄存在,則執行update語句而不是insert語句,反之,則執行insert語句而不是更新語句。
比如我建立表的時候設定的唯一索引為欄位(a,b,c),那麼當a,b,c三個欄位完全重複時候,此時就要執行更新語句。當然滿足一部分唯一索引是不會觸發更新操作的,此時會執行插入操作。
而至於要更新哪些欄位,要看我們自己的需求了。
3.1、ON DUPLICATE KEY UPDATE 實踐
先宣告一點:ON DUPLICATE KEY UPDATE 這個子句是MySQL特有的,語句的作用是,當insert已經存在的記錄時,就執行update。
舉例說明:
user表中有一條資料如下:
表中的主鍵為id,現要插入一條id為2的資料,正常寫法為:
insert into user(id,user_id,user_name,email,address,create_time,update_time) values(2,3764,'李四','[email protected]','北京市東城區',now(),now());
執行後重新整理表資料,我們來看錶中內容:
此時表中資料增加了一條id為2的記錄,當我們再次執行插入語句時,會發生什麼呢?
Mysql告訴我們,我們的主鍵衝突了,看到這裡我們是不是可以改變一下思路,當插入已存在主鍵的記錄時,將插入操作變為修改:
// 在原sql後面增加 ON DUPLICATE KEY UPDATE insert into user(id,user_id,user_name,email,address,create_time,update_time) values(2,3764,'李四','[email protected]','北京市東城區',now(),now()) ON DUPLICATE KEY UPDATE user_name='王五',email='[email protected]',address='河北省保定市';
我們執行上面的sql,並重新整理表:
可以看到原有的資料被修改了,而不是執行插入。原本id為2的記錄,改為了'王五','[email protected]','河北省保定市',很好的解決了重複插入問題。
3.2、VALUES修改
那麼問題來了,有人會說我ON DUPLICATE KEY UPDATE 後面跟的是固定的值,如果我想要分別給不同的記錄插入不同的值怎麼辦呢?
insert into user(id,user_id,user_name,email,address,create_time,update_time) values(2,3764,'孫六','[email protected]','上海市紅橋區',now(),now()) ON DUPLICATE KEY UPDATE user_name=VALUES(user_name),email=VALUES(email),address=VALUES(address);
可以將後面的修改條件改為使用VALUES()函式,動態的傳入要修改的值,執行上述sql,並重新整理表:
四、總結
以上介紹的是addOrUpdate的語義,其實修改的方法有很多種,包括SET或用REPLACE,連事務都省的做,ON DUPLICATE KEY UPDATE能夠讓我們便捷的完成重複插入的開發需求,但它是Mysql的特有語法,使用時應多注意主鍵和插入值是否是我們想要插入或修改的資料。
即便如此,在實際開發中,我們仍然不推薦這種寫法,因為這種寫法耦合了add和update兩種操作,線上出現bug時,極難定位問題。推薦的做法是:單寫一個add方法,只負責插入資料,插入重複資料時,根據業務場景做冪等性處理;單寫一個update方法,只負責更新操作。兩個函式單獨打自己的log,便於定位問題。