1. 程式人生 > 其它 >gorm.Clause()子句分析之ON DUPLICATE KEY UPDATE

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 在Querycallback 中構建最終的查詢 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,便於定位問題。