<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
CreateOrUpdate 是業務開發中很常見的場景,我們支援使用者對某個業務實體進行建立/設定。希望實現的 repository 介面要達到以下兩個要求:
根據筆者的團隊合作經驗看,很多 Golang 開發同學不是很確定對於這種場景到底怎麼實現,寫出來的程式碼五花八門,還可能有並行問題。今天我們就來看看基於 GORM 怎麼來實現 CreateOrUpdate。
我們先來看下 GORM 提供了那些方法來支援我們往資料庫插入資料,對 GORM 比較熟悉的同學可以忽略這部分:
插入一條記錄到資料庫,注意需要通過資料的指標來建立,回填主鍵;
// 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 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 }
關注點:
事實上有一些業務場景下,我們可以用 Save 來實現 CreateOrUpdate 的語意:
但 Save 本身語意其實比較混亂,不太建議使用,把這部分留給業務自己實現,用Updates,Create用起來更明確些。
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。
獲取第一條匹配的記錄,或者根據給定的條件初始化一個範例(僅支援 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 函數。這裡的定位是一個純粹的讀介面。
獲取第一條匹配的記錄,或者根據給定的條件建立一條新紀錄(僅支援 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 方法查詢,後續的處理很關鍵:
第一個分支好理解,需要插入新資料。重點在於 else if len(db.Statement.assigns) > 0
分支。
我們呼叫 FirstOrCreate
時,需要傳入一個物件,再傳入一批條件,這批條件會作為 Where 語句的部分在一開始進行查詢。而這個函數同時可以配合 Assign()
使用,這一點就賦予了生命力。
不管是否找到記錄,Assign
都會將屬性賦值給 struct,並將結果寫回資料庫。
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。
鑑於 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 }
初階的用法中,我們只需要關注三個屬性:
type Set []Assignment type Assignment struct { Column Column Value interface{} }
需要注意的是,所謂 OnConflict,並不一定是主鍵衝突,唯一鍵也包含在內。所以,使用 OnConflict 這套 Upsert 的先決條件是【唯一索引】或【主鍵】都可以。生成一條SQL語句,並行安全。
如果沒有唯一索引的限制,我們就無法複用這個能力,需要考慮別的解法。如果
以上就是基於GORM實現CreateOrUpdate方法詳解的詳細內容,更多關於GORM CreateOrUpdate方法的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45