網站首頁 編程語言 正文
正文
CreateOrUpdate 是業務開發中很常見的場景,我們支持用戶對某個業務實體進行創建/配置。希望實現的 repository 接口要達到以下兩個要求:
- 如果此前不存在該實體,創建一個新的;
- 如果此前該實體已經存在,更新相關屬性。
根據筆者的團隊合作經驗看,很多 Golang 開發同學不是很確定對于這種場景到底怎么實現,寫出來的代碼五花八門,還可能有并發問題。今天我們就來看看基于 GORM 怎么來實現 CreateOrUpdate。
GORM 寫接口原理
我們先來看下 GORM 提供了那些方法來支持我們往數據庫插入數據,對 GORM 比較熟悉的同學可以忽略這部分:
Create
插入一條記錄到數據庫,注意需要通過數據的指針來創建,回填主鍵;
// Create insert the value into database func (db *DB) Create(value interface{}) (tx *DB) { if db.CreateBatchSize > 0 { return db.CreateInBatches(value, db.CreateBatchSize) } tx = db.getInstance() tx.Statement.Dest = value return tx.callbacks.Create().Execute(tx) }
賦值 Dest 后直接進入 Create 的 callback 流程。
Save
保存所有的字段,即使字段是零值。如果我們傳入的結構主鍵為零值,則會插入記錄。
// Save update value in database, if the value doesn't have primary key, will insert it func (db *DB) Save(value interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = value reflectValue := reflect.Indirect(reflect.ValueOf(value)) for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface { reflectValue = reflect.Indirect(reflectValue) } switch reflectValue.Kind() { case reflect.Slice, reflect.Array: if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok { tx = tx.Clauses(clause.OnConflict{UpdateAll: true}) } tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true)) case reflect.Struct: if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil { for _, pf := range tx.Statement.Schema.PrimaryFields { if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero { return tx.callbacks.Create().Execute(tx) } } } fallthrough default: selectedUpdate := len(tx.Statement.Selects) != 0 // when updating, use all fields including those zero-value fields if !selectedUpdate { tx.Statement.Selects = append(tx.Statement.Selects, "*") } tx = tx.callbacks.Update().Execute(tx) if tx.Error == nil && tx.RowsAffected == 0 && !tx.DryRun && !selectedUpdate { result := reflect.New(tx.Statement.Schema.ModelType).Interface() if result := tx.Session(&Session{}).Limit(1).Find(result); result.RowsAffected == 0 { return tx.Create(value) } } } return }
關注點:
- 在 reflect.Struct 的分支,判斷 PrimaryFields 也就是主鍵列是否為零值,如果是,直接開始調用 Create 的 callback,這也和 Save 的說明匹配;
- switch 里面用到了 fallthrough 關鍵字,說明 switch 命中后繼續往下命中 default;
- 如果我們沒有用 Select() 方法指定需要更新的字段,則默認是全部更新,包含所有零值字段,這里用的通配符 *
- 如果主鍵不為零值,說明記錄已經存在,這個時候就會去更新。
事實上有一些業務場景下,我們可以用 Save 來實現 CreateOrUpdate 的語義:
- 首次調用時主鍵ID為空,這時 Save 會走到 Create 分支去插入數據。
- 隨后調用時存在主鍵ID,觸發更新邏輯。
但 Save 本身語義其實比較混亂,不太建議使用,把這部分留給業務自己實現,用Updates,Create用起來更明確些。
Update & Updates
Update 前者更新單個列。
Updates 更新多列,且當使用 struct 更新時,默認情況下,GORM 只會更新非零值的字段(可以用 Select 指定來解這個問題)。使用 map 更新時則會全部更新。
// Update update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fields func (db *DB) Update(column string, value interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = map[string]interface{}{column: value} return tx.callbacks.Update().Execute(tx) } // Updates update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fields func (db *DB) Updates(values interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = values return tx.callbacks.Update().Execute(tx) }
這里也能從實現中看出來一些端倪。Update 接口內部是封裝了一個 map[string]interface{},而 Updates 則是可以接受 map 也可以走 struct,最終寫入 Dest。
FirstOrInit
獲取第一條匹配的記錄,或者根據給定的條件初始化一個實例(僅支持 struct 和 map)
// FirstOrInit gets the first matched record or initialize a new instance with given conditions (only works with struct or map conditions) func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) { queryTx := db.Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, }) if tx = queryTx.Find(dest, conds...); tx.RowsAffected == 0 { if c, ok := tx.Statement.Clauses["WHERE"]; ok { if where, ok := c.Expression.(clause.Where); ok { tx.assignInterfacesToValue(where.Exprs) } } // initialize with attrs, conds if len(tx.Statement.attrs) > 0 { tx.assignInterfacesToValue(tx.Statement.attrs...) } } // initialize with attrs, conds if len(tx.Statement.assigns) > 0 { tx.assignInterfacesToValue(tx.Statement.assigns...) } return }
注意,Init 和 Create 的區別,如果沒有找到,這里會把實例給初始化,不會存入 DB,可以看到 RowsAffected == 0 分支的處理,這里并不會走 Create 的 callback 函數。這里的定位是一個純粹的讀接口。
FirstOrCreate
獲取第一條匹配的記錄,或者根據給定的條件創建一條新紀錄(僅支持 struct 和 map 條件)。FirstOrCreate可能會執行兩條sql,他們是一個事務中的。
// FirstOrCreate gets the first matched record or create a new one with given conditions (only works with struct, map conditions) func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() queryTx := db.Session(&Session{}).Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, }) if result := queryTx.Find(dest, conds...); result.Error == nil { if result.RowsAffected == 0 { if c, ok := result.Statement.Clauses["WHERE"]; ok { if where, ok := c.Expression.(clause.Where); ok { result.assignInterfacesToValue(where.Exprs) } } // initialize with attrs, conds if len(db.Statement.attrs) > 0 { result.assignInterfacesToValue(db.Statement.attrs...) } // initialize with attrs, conds if len(db.Statement.assigns) > 0 { result.assignInterfacesToValue(db.Statement.assigns...) } return tx.Create(dest) } else if len(db.Statement.assigns) > 0 { exprs := tx.Statement.BuildCondition(db.Statement.assigns[0], db.Statement.assigns[1:]...) assigns := map[string]interface{}{} for _, expr := range exprs { if eq, ok := expr.(clause.Eq); ok { switch column := eq.Column.(type) { case string: assigns[column] = eq.Value case clause.Column: assigns[column.Name] = eq.Value default: } } } return tx.Model(dest).Updates(assigns) } } else { tx.Error = result.Error } return tx }
注意區別,同樣是構造 queryTx 去調用 Find 方法查詢,后續的處理很關鍵:
- 若沒有查到結果,將 where 條件,Attrs() 以及 Assign() 方法賦值的屬性寫入對象,從源碼可以看到是通過三次 assignInterfacesToValue 實現的。屬性更新后,調用 Create 方法往數據庫中插入;
- 若查到了結果,但 Assign() 此前已經寫入了一些屬性,就將其寫入對象,進行 Updates 調用。
第一個分支好理解,需要插入新數據。重點在于 else if len(db.Statement.assigns) > 0
分支。
我們調用 FirstOrCreate
時,需要傳入一個對象,再傳入一批條件,這批條件會作為 Where 語句的部分在一開始進行查詢。而這個函數同時可以配合 Assign()
使用,這一點就賦予了生命力。
不管是否找到記錄,Assign
?都會將屬性賦值給 struct,并將結果寫回數據庫。
方案一:FirstOrCreate + Assign
func (db *DB) Attrs(attrs ...interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.attrs = attrs return } func (db *DB) Assign(attrs ...interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.assigns = attrs return }
這種方式充分利用了 Assign 的能力。我們在上面 FirstOrCreate 的分析中可以看出,這里是會將 Assign 進來的屬性應用到 struct 上,寫入數據庫的。區別只在于是插入(Insert)還是更新(Update)。
// 未找到 user,根據條件和 Assign 屬性創建記錄 db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) // SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1; // INSERT INTO "users" (name, age) VALUES ("non_existing", 20); // user -> User{ID: 112, Name: "non_existing", Age: 20} // 找到了 `name` = `jinzhu` 的 user,依然會根據 Assign 更新記錄 db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user) // SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1; // UPDATE users SET age=20 WHERE id = 111; // user -> User{ID: 111, Name: "jinzhu", Age: 20}
所以,要實現 CreateOrUpdate,我們可以將需要 Update 的屬性通過 Assign 函數放進來,隨后如果通過 Where 找到了記錄,也會將 Assign 屬性應用上,隨后 Update。
這樣的思路一定是可以跑通的,但使用之前要看場景。
為什么?
因為參看上面源碼我們就知道,FirstOrCreate 本質是 Select + Insert 或者 Select + Update。
無論怎樣,都是兩條 SQL,可能有并發安全問題。如果你的業務場景不存在并發,可以放心用 FirstOrCreate + Assign,功能更多,適配更多場景。
而如果可能有并發安全的坑,我們就要考慮方案二:Upsert。
方案二:Upsert
鑒于 MySQL 提供了 ON DUPLICATE KEY UPDATE
的能力,我們可以充分利用唯一鍵的約束,來搞定并發場景下的 CreateOrUpdate。
import "gorm.io/gorm/clause" // 不處理沖突 DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) // `id` 沖突時,將字段值更新為默認值 DB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}), }).Create(&users) // MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server // INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL // Update columns to new value on `id` conflict DB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), }).Create(&users) // MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server // INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL // INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL
這里依賴了 GORM 的 Clauses 方法,我們來看一下:
type Interface interface { Name() string Build(Builder) MergeClause(*Clause) } // AddClause add clause func (stmt *Statement) AddClause(v clause.Interface) { if optimizer, ok := v.(StatementModifier); ok { optimizer.ModifyStatement(stmt) } else { name := v.Name() c := stmt.Clauses[name] c.Name = name v.MergeClause(&c) stmt.Clauses[name] = c } }
這里添加進來一個 Clause 之后,會調用 MergeClause 將語句進行合并,而 OnConflict 的適配是這樣:
package clause type OnConflict struct { Columns []Column Where Where TargetWhere Where OnConstraint string DoNothing bool DoUpdates Set UpdateAll bool } func (OnConflict) Name() string { return "ON CONFLICT" } // Build build onConflict clause func (onConflict OnConflict) Build(builder Builder) { if len(onConflict.Columns) > 0 { builder.WriteByte('(') for idx, column := range onConflict.Columns { if idx > 0 { builder.WriteByte(',') } builder.WriteQuoted(column) } builder.WriteString(`) `) } if len(onConflict.TargetWhere.Exprs) > 0 { builder.WriteString(" WHERE ") onConflict.TargetWhere.Build(builder) builder.WriteByte(' ') } if onConflict.OnConstraint != "" { builder.WriteString("ON CONSTRAINT ") builder.WriteString(onConflict.OnConstraint) builder.WriteByte(' ') } if onConflict.DoNothing { builder.WriteString("DO NOTHING") } else { builder.WriteString("DO UPDATE SET ") onConflict.DoUpdates.Build(builder) } if len(onConflict.Where.Exprs) > 0 { builder.WriteString(" WHERE ") onConflict.Where.Build(builder) builder.WriteByte(' ') } } // MergeClause merge onConflict clauses func (onConflict OnConflict) MergeClause(clause *Clause) { clause.Expression = onConflict }
初階的用法中,我們只需要關注三個屬性:
- DoNothing:沖突后不處理,參照上面的 Build 實現可以看到,這里只會加入 DO NOTHING;
- DoUpdates: 配置一批需要賦值的 KV,如果沒有指定 DoNothing,會根據這一批 Assignment 來寫入要更新的列和值;
type Set []Assignment type Assignment struct { Column Column Value interface{} }
- UpdateAll: 沖突后更新所有的值(非 default tag字段)。
需要注意的是,所謂 OnConflict,并不一定是主鍵沖突,唯一鍵也包含在內。所以,使用 OnConflict 這套 Upsert 的先決條件是【唯一索引】或【主鍵】都可以。生成一條SQL語句,并發安全。
如果沒有唯一索引的限制,我們就無法復用這個能力,需要考慮別的解法。如果
總結
- 若你的 CreateOrUpdate 能用到【唯一索引】或【主鍵】,建議使用方案二,這也是作者金柱大佬最推薦的方案,并發安全;
- 若無法用【唯一索引】來限制,需要用其他列來判斷,且不關注并發安全,可以采用方案一;
- 若只需要按照【主鍵】是否為零值來實現 CreateOrUpdate,可以使用 Save(接口語義不是特別明確,用的時候小心,如果可以,盡量用 Create/Update)。
原文鏈接:https://juejin.cn/post/7155840164504272933
相關推薦
- 2022-04-19 運行 npm run xxx 的時候都執行了些什么
- 2023-04-03 python之語句mode?=?'test'?if?y?is?None?else?'train'問題
- 2022-07-13 Sybase使用Spring的只讀事物報 Use ‘set readonly off‘ to exe
- 2022-05-12 C++線程安全的隊列你了解嘛_C 語言
- 2022-07-08 一文詳解Python中生成器的原理與使用_python
- 2023-06-20 Jupyter?Notebook中%time和%timeit的使用詳解_python
- 2022-08-07 C#實現關機功能_C#教程
- 2022-07-22 C語言輸出所有水仙花數
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支