溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

基于GORM如何實(shí)現(xiàn)CreateOrUpdate

發(fā)布時(shí)間:2022-10-25 09:23:41 來(lái)源:億速云 閱讀:169 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要講解了“基于GORM如何實(shí)現(xiàn)CreateOrUpdate”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“基于GORM如何實(shí)現(xiàn)CreateOrUpdate”吧!

    GORM 寫(xiě)接口原理

    我們先來(lái)看下 GORM 提供了那些方法來(lái)支持我們往數(shù)據(jù)庫(kù)插入數(shù)據(jù),對(duì) GORM 比較熟悉的同學(xué)可以忽略這部分:

    Create

    插入一條記錄到數(shù)據(jù)庫(kù),注意需要通過(guò)數(shù)據(jù)的指針來(lái)創(chuàng)建,回填主鍵;

    // 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 后直接進(jìn)入 Create 的 callback 流程。

    Save

    保存所有的字段,即使字段是零值。如果我們傳入的結(jié)構(gòu)主鍵為零值,則會(huì)插入記錄。

    // 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
    }

    關(guān)注點(diǎn):

    • 在 reflect.Struct 的分支,判斷 PrimaryFields 也就是主鍵列是否為零值,如果是,直接開(kāi)始調(diào)用 Create 的 callback,這也和 Save 的說(shuō)明匹配;

    • switch 里面用到了 fallthrough 關(guān)鍵字,說(shuō)明 switch 命中后繼續(xù)往下命中 default;

    • 如果我們沒(méi)有用 Select() 方法指定需要更新的字段,則默認(rèn)是全部更新,包含所有零值字段,這里用的通配符 *

    • 如果主鍵不為零值,說(shuō)明記錄已經(jīng)存在,這個(gè)時(shí)候就會(huì)去更新。

    事實(shí)上有一些業(yè)務(wù)場(chǎng)景下,我們可以用 Save 來(lái)實(shí)現(xiàn) CreateOrUpdate 的語(yǔ)義:

    • 首次調(diào)用時(shí)主鍵ID為空,這時(shí) Save 會(huì)走到 Create 分支去插入數(shù)據(jù)。

    • 隨后調(diào)用時(shí)存在主鍵ID,觸發(fā)更新邏輯。

    但 Save 本身語(yǔ)義其實(shí)比較混亂,不太建議使用,把這部分留給業(yè)務(wù)自己實(shí)現(xiàn),用Updates,Create用起來(lái)更明確些。

    Update & Updates

    Update 前者更新單個(gè)列。

    Updates 更新多列,且當(dāng)使用 struct 更新時(shí),默認(rèn)情況下,GORM 只會(huì)更新非零值的字段(可以用 Select 指定來(lái)解這個(gè)問(wèn)題)。使用 map 更新時(shí)則會(huì)全部更新。

    // 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)
    }

    這里也能從實(shí)現(xiàn)中看出來(lái)一些端倪。Update 接口內(nèi)部是封裝了一個(gè) map[string]interface{},而 Updates 則是可以接受 map 也可以走 struct,最終寫(xiě)入 Dest。

    FirstOrInit

    獲取第一條匹配的記錄,或者根據(jù)給定的條件初始化一個(gè)實(shí)例(僅支持 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 的區(qū)別,如果沒(méi)有找到,這里會(huì)把實(shí)例給初始化,不會(huì)存入 DB,可以看到 RowsAffected == 0 分支的處理,這里并不會(huì)走 Create 的 callback 函數(shù)。這里的定位是一個(gè)純粹的讀接口。

    FirstOrCreate

    獲取第一條匹配的記錄,或者根據(jù)給定的條件創(chuàng)建一條新紀(jì)錄(僅支持 struct 和 map 條件)。FirstOrCreate可能會(huì)執(zhí)行兩條sql,他們是一個(gè)事務(wù)中的。

    // 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
    }

    注意區(qū)別,同樣是構(gòu)造 queryTx 去調(diào)用 Find 方法查詢(xún),后續(xù)的處理很關(guān)鍵:

    • 若沒(méi)有查到結(jié)果,將 where 條件,Attrs() 以及 Assign() 方法賦值的屬性寫(xiě)入對(duì)象,從源碼可以看到是通過(guò)三次 assignInterfacesToValue 實(shí)現(xiàn)的。屬性更新后,調(diào)用 Create 方法往數(shù)據(jù)庫(kù)中插入;

    • 若查到了結(jié)果,但 Assign() 此前已經(jīng)寫(xiě)入了一些屬性,就將其寫(xiě)入對(duì)象,進(jìn)行 Updates 調(diào)用。

    第一個(gè)分支好理解,需要插入新數(shù)據(jù)。重點(diǎn)在于 else if len(db.Statement.assigns) > 0 分支。

    我們調(diào)用 FirstOrCreate 時(shí),需要傳入一個(gè)對(duì)象,再傳入一批條件,這批條件會(huì)作為 Where 語(yǔ)句的部分在一開(kāi)始進(jìn)行查詢(xún)。而這個(gè)函數(shù)同時(shí)可以配合 Assign() 使用,這一點(diǎn)就賦予了生命力。

    不管是否找到記錄,Assign 都會(huì)將屬性賦值給 struct,并將結(jié)果寫(xiě)回?cái)?shù)據(jù)庫(kù)。

    方案一: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 的能力。我們?cè)谏厦?FirstOrCreate 的分析中可以看出,這里是會(huì)將 Assign 進(jìn)來(lái)的屬性應(yīng)用到 struct 上,寫(xiě)入數(shù)據(jù)庫(kù)的。區(qū)別只在于是插入(Insert)還是更新(Update)。

    // 未找到 user,根據(jù)條件和 Assign 屬性創(chuàng)建記錄
    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,依然會(huì)根據(jù) 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}

    所以,要實(shí)現(xiàn) CreateOrUpdate,我們可以將需要 Update 的屬性通過(guò) Assign 函數(shù)放進(jìn)來(lái),隨后如果通過(guò) Where 找到了記錄,也會(huì)將 Assign 屬性應(yīng)用上,隨后 Update。

    這樣的思路一定是可以跑通的,但使用之前要看場(chǎng)景。

    為什么?

    因?yàn)閰⒖瓷厦嬖创a我們就知道,F(xiàn)irstOrCreate 本質(zhì)是 Select + Insert 或者 Select + Update。

    無(wú)論怎樣,都是兩條 SQL,可能有并發(fā)安全問(wèn)題。如果你的業(yè)務(wù)場(chǎng)景不存在并發(fā),可以放心用 FirstOrCreate + Assign,功能更多,適配更多場(chǎng)景。

    而如果可能有并發(fā)安全的坑,我們就要考慮方案二:Upsert。

    方案二:Upsert

    鑒于 MySQL 提供了 ON DUPLICATE KEY UPDATE 的能力,我們可以充分利用唯一鍵的約束,來(lái)搞定并發(fā)場(chǎng)景下的 CreateOrUpdate。

    import "gorm.io/gorm/clause"
    // 不處理沖突
    DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
    // `id` 沖突時(shí),將字段值更新為默認(rèn)值
    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

    這里依賴(lài)了 GORM 的 Clauses 方法,我們來(lái)看一下:

    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
    	}
    }

    這里添加進(jìn)來(lái)一個(gè) Clause 之后,會(huì)調(diào)用 MergeClause 將語(yǔ)句進(jìn)行合并,而 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
    }

    初階的用法中,我們只需要關(guān)注三個(gè)屬性:

    • DoNothing:沖突后不處理,參照上面的 Build 實(shí)現(xiàn)可以看到,這里只會(huì)加入 DO NOTHING;

    • DoUpdates: 配置一批需要賦值的 KV,如果沒(méi)有指定 DoNothing,會(huì)根據(jù)這一批 Assignment 來(lái)寫(xiě)入要更新的列和值;

    type Set []Assignment
    type Assignment struct {
    	Column Column
    	Value  interface{}
    }
    • UpdateAll: 沖突后更新所有的值(非 default tag字段)。

    需要注意的是,所謂 OnConflict,并不一定是主鍵沖突,唯一鍵也包含在內(nèi)。所以,使用 OnConflict 這套 Upsert 的先決條件是【唯一索引】或【主鍵】都可以。生成一條SQL語(yǔ)句,并發(fā)安全。

    如果沒(méi)有唯一索引的限制,我們就無(wú)法復(fù)用這個(gè)能力,需要考慮別的解法。

    感謝各位的閱讀,以上就是“基于GORM如何實(shí)現(xiàn)CreateOrUpdate”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)基于GORM如何實(shí)現(xiàn)CreateOrUpdate這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI