亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

基于GORM實現(xiàn)CreateOrUpdate方法詳解

 更新時間:2022年10月20日 16:31:07   作者:ag9920  
這篇文章主要為大家介紹了基于GORM實現(xiàn)CreateOrUpdate方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

正文

CreateOrUpdate 是業(yè)務(wù)開發(fā)中很常見的場景,我們支持用戶對某個業(yè)務(wù)實體進(jìn)行創(chuàng)建/配置。希望實現(xiàn)的 repository 接口要達(dá)到以下兩個要求:

  • 如果此前不存在該實體,創(chuàng)建一個新的;
  • 如果此前該實體已經(jīng)存在,更新相關(guān)屬性。

根據(jù)筆者的團(tuán)隊合作經(jīng)驗看,很多 Golang 開發(fā)同學(xué)不是很確定對于這種場景到底怎么實現(xiàn),寫出來的代碼五花八門,還可能有并發(fā)問題。今天我們就來看看基于 GORM 怎么來實現(xiàn) CreateOrUpdate。

GORM 寫接口原理

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

Create

插入一條記錄到數(shù)據(jù)庫,注意需要通過數(shù)據(jù)的指針來創(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)主鍵為零值,則會插入記錄。

// 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)注點:

  • 在 reflect.Struct 的分支,判斷 PrimaryFields 也就是主鍵列是否為零值,如果是,直接開始調(diào)用 Create 的 callback,這也和 Save 的說明匹配;
  • switch 里面用到了 fallthrough 關(guān)鍵字,說明 switch 命中后繼續(xù)往下命中 default;
  • 如果我們沒有用 Select() 方法指定需要更新的字段,則默認(rèn)是全部更新,包含所有零值字段,這里用的通配符 *
  • 如果主鍵不為零值,說明記錄已經(jīng)存在,這個時候就會去更新。

事實上有一些業(yè)務(wù)場景下,我們可以用 Save 來實現(xiàn) CreateOrUpdate 的語義:

  • 首次調(diào)用時主鍵ID為空,這時 Save 會走到 Create 分支去插入數(shù)據(jù)。
  • 隨后調(diào)用時存在主鍵ID,觸發(fā)更新邏輯。

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

Update & Updates

Update 前者更新單個列。

Updates 更新多列,且當(dāng)使用 struct 更新時,默認(rèn)情況下,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)
}

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

FirstOrInit

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

FirstOrCreate

獲取第一條匹配的記錄,或者根據(jù)給定的條件創(chuàng)建一條新紀(jì)錄(僅支持 struct 和 map 條件)。FirstOrCreate可能會執(zhí)行兩條sql,他們是一個事務(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ù)的處理很關(guān)鍵:

  • 若沒有查到結(jié)果,將 where 條件,Attrs() 以及 Assign() 方法賦值的屬性寫入對象,從源碼可以看到是通過三次 assignInterfacesToValue 實現(xiàn)的。屬性更新后,調(diào)用 Create 方法往數(shù)據(jù)庫中插入;
  • 若查到了結(jié)果,但 Assign() 此前已經(jīng)寫入了一些屬性,就將其寫入對象,進(jìn)行 Updates 調(diào)用。

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

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

不管是否找到記錄,Assign 都會將屬性賦值給 struct,并將結(jié)果寫回數(shù)據(jù)庫。

方案一: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 進(jìn)來的屬性應(yīng)用到 struct 上,寫入數(shù)據(jù)庫的。區(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,依然會根據(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}

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

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

為什么?

因為參看上面源碼我們就知道,F(xiàn)irstOrCreate 本質(zhì)是 Select + Insert 或者 Select + Update。

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

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

方案二:Upsert

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

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

這里依賴了 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
	}
}

這里添加進(jìn)來一個 Clause 之后,會調(diào)用 MergeClause 將語句進(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)注三個屬性:

  • DoNothing:沖突后不處理,參照上面的 Build 實現(xiàn)可以看到,這里只會加入 DO NOTHING;
  • DoUpdates: 配置一批需要賦值的 KV,如果沒有指定 DoNothing,會根據(jù)這一批 Assignment 來寫入要更新的列和值;
type Set []Assignment
type Assignment struct {
	Column Column
	Value  interface{}
}
  • UpdateAll: 沖突后更新所有的值(非 default tag字段)。

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

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

總結(jié)

  • 若你的 CreateOrUpdate 能用到【唯一索引】或【主鍵】,建議使用方案二,這也是作者金柱大佬最推薦的方案,并發(fā)安全;
  • 若無法用【唯一索引】來限制,需要用其他列來判斷,且不關(guān)注并發(fā)安全,可以采用方案一;
  • 若只需要按照【主鍵】是否為零值來實現(xiàn) CreateOrUpdate,可以使用 Save(接口語義不是特別明確,用的時候小心,如果可以,盡量用 Create/Update)。

以上就是基于GORM實現(xiàn)CreateOrUpdate方法詳解的詳細(xì)內(nèi)容,更多關(guān)于GORM CreateOrUpdate方法的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Windows下CMD執(zhí)行Go出現(xiàn)中文亂碼的解決方法

    Windows下CMD執(zhí)行Go出現(xiàn)中文亂碼的解決方法

    本文主要介紹了Windows下CMD執(zhí)行Go出現(xiàn)中文亂碼的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • golang基礎(chǔ)之waitgroup用法以及使用要點

    golang基礎(chǔ)之waitgroup用法以及使用要點

    WaitGroup是Golang并發(fā)的兩種方式之一,一個是Channel,另一個是WaitGroup,下面這篇文章主要給大家介紹了關(guān)于golang基礎(chǔ)之waitgroup用法以及使用要點的相關(guān)資料,需要的朋友可以參考下
    2023-01-01
  • go日志庫logrus的安裝及快速使用

    go日志庫logrus的安裝及快速使用

    這篇文章主要為大家介紹了go日志庫logrus的安裝及快速使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • Go實現(xiàn)SMTP郵件發(fā)送訂閱功能(包含163郵箱、163企業(yè)郵箱、谷歌gmail郵箱)

    Go實現(xiàn)SMTP郵件發(fā)送訂閱功能(包含163郵箱、163企業(yè)郵箱、谷歌gmail郵箱)

    這篇文章給大家介紹了Go實現(xiàn)SMTP郵件發(fā)送訂閱功能(包含163郵箱、163企業(yè)郵箱、谷歌gmail郵箱),需求很簡單,就是用戶輸入自己的郵箱后,使用官方郵箱給用戶發(fā)送替郵件模版,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下
    2023-10-10
  • Go語言實現(xiàn)基于websocket瀏覽器通知功能

    Go語言實現(xiàn)基于websocket瀏覽器通知功能

    這篇文章主要介紹了Go語言實現(xiàn)基于websocket瀏覽器通知功能,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-07-07
  • 一文帶你了解Go語言如何解析JSON

    一文帶你了解Go語言如何解析JSON

    本文將說明如何利用?Go?語言將?JSON?解析為結(jié)構(gòu)體和數(shù)組,如果解析?JSON?的嵌入對象,如何將?JSON?的自定義屬性名稱映射到結(jié)構(gòu)體,如何解析非結(jié)構(gòu)化的?JSON?字符串
    2023-01-01
  • 詳解Golang如何實現(xiàn)一個環(huán)形緩沖器

    詳解Golang如何實現(xiàn)一個環(huán)形緩沖器

    環(huán)形緩沖器(ringr?buffer)是一種用于表示一個固定尺寸、頭尾相連的緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),適合緩存數(shù)據(jù)流。本文將利用Golang實現(xiàn)一個環(huán)形緩沖器,需要的可以參考一下
    2022-09-09
  • 詳解Golang利用反射reflect動態(tài)調(diào)用方法

    詳解Golang利用反射reflect動態(tài)調(diào)用方法

    這篇文章主要介紹了詳解Golang利用反射reflect動態(tài)調(diào)用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-11-11
  • Go語言crypto包創(chuàng)建自己的密碼加密工具實現(xiàn)示例

    Go語言crypto包創(chuàng)建自己的密碼加密工具實現(xiàn)示例

    Go語言借助它的簡單性和強(qiáng)大的標(biāo)準(zhǔn)庫,實現(xiàn)一個自己的密碼加密工具,本文將會結(jié)合代碼示例深入探討如何使用Go語言的crypto包來實現(xiàn)自己的加密工具
    2023-11-11
  • Go并發(fā)編程sync.Cond的具體使用

    Go并發(fā)編程sync.Cond的具體使用

    Go 標(biāo)準(zhǔn)庫提供 Cond 原語的目的是,為等待 / 通知場景下的并發(fā)問題提供支持,本文主要介紹了Go并發(fā)編程sync.Cond的具體使用,具有一定的參考價值,感興趣的可以了解一下
    2022-05-05

最新評論