一、背景介紹
最近看到一段代碼,使用到了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,'test123@qq.com','北京市海淀區','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`)
使用這條語句的原因,是為了更好的執行插入和更新,因為我們在插入一條數據時,表中可能已經存在了這條數據,這時我們想實現更新的功能,或者表中沒有這條數據,我們想實現插入的功能,而上面這條sql語句可以同時實現插入和更新的功能。
那么這條sql語句是如何解釋呢?我們很容易理解前面的部分,就是一個簡單的插入語句,讓我們看下后面的部分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,'李四','lisi@qq.com','北京市東城區',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,'李四','lisi@qq.com','北京市東城區',now(),now()) ON DUPLICATE KEY UPDATE user_name='王五',email='wangwu@qq.com',address='河北省保定市';
我們執行上面的sql,並刷新表:

可以看到原有的數據被修改了,而不是執行插入。原本id為2的記錄,改為了'王五','wangwu@qq.com','河北省保定市',很好的解決了重復插入問題。
3.2、VALUES修改
那么問題來了,有人會說我ON DUPLICATE KEY UPDATE 后面跟的是固定的值,如果我想要分別給不同的記錄插入不同的值怎么辦呢?
insert into user(id,user_id,user_name,email,address,create_time,update_time) values(2,3764,'孫六','sunliu@qq.com','上海市紅橋區',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,便於定位問題。
