基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解_第1頁
基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解_第2頁
基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解_第3頁
基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解_第4頁
基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解_第5頁
已閱讀5頁,還剩9頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

第基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解目錄正文GORM寫接口原理CreateSaveUpdateUpdatesFirstOrInitFirstOrCreate方案一:FirstOrCreate+Assign方案二:Upsert總結(jié)

正文

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

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

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

GORM寫接口原理

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

Create

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

//Createinsertthevalueintodatabase

func(db*DB)Create(valueinterface{})(tx*DB){

ifdb.CreateBatchSize0{

returndb.CreateInBatches(value,db.CreateBatchSize)

tx=db.getInstance()

tx.Statement.Dest=value

returntx.callbacks.Create().Execute(tx)

賦值Dest后直接進(jìn)入Create的callback流程。

Save

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

//Saveupdatevalueindatabase,ifthevaluedoesn'thaveprimarykey,willinsertit

func(db*DB)Save(valueinterface{})(tx*DB){

tx=db.getInstance()

tx.Statement.Dest=value

reflectValue:=reflect.Indirect(reflect.ValueOf(value))

forreflectValue.Kind()==reflect.Ptr||reflectValue.Kind()==reflect.Interface{

reflectValue=reflect.Indirect(reflectValue)

switchreflectValue.Kind(){

casereflect.Slice,reflect.Array:

if_,ok:=tx.Statement.Clauses["ONCONFLICT"];!ok{

tx=tx.Clauses(clause.OnConflict{UpdateAll:true})

tx=tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time",true))

casereflect.Struct:

iferr:=tx.Statement.Parse(value);err==niltx.Statement.Schema!=nil{

for_,pf:=rangetx.Statement.Schema.PrimaryFields{

if_,isZero:=pf.ValueOf(tx.Statement.Context,reflectValue);isZero{

returntx.callbacks.Create().Execute(tx)

fallthrough

default:

selectedUpdate:=len(tx.Statement.Selects)!=0

//whenupdating,useallfieldsincludingthosezero-valuefields

if!selectedUpdate{

tx.Statement.Selects=append(tx.Statement.Selects,"*")

tx=tx.callbacks.Update().Execute(tx)

iftx.Error==niltx.RowsAffected==0!tx.DryRun!selectedUpdate{

result:=reflect.New(tx.Statement.Schema.ModelType).Interface()

ifresult:=tx.Session(Session{}).Limit(1).Find(result);result.RowsAffected==0{

returntx.Create(value)

return

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

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

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

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

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

UpdateUpdates

Update前者更新單個列。

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

//Updateupdateattributeswithcallbacks,refer:https://gorm.io/docs/update.html#Update-Changed-Fields

func(db*DB)Update(columnstring,valueinterface{})(tx*DB){

tx=db.getInstance()

tx.Statement.Dest=map[string]interface{}{column:value}

returntx.callbacks.Update().Execute(tx)

//Updatesupdateattributeswithcallbacks,refer:https://gorm.io/docs/update.html#Update-Changed-Fields

func(db*DB)Updates(valuesinterface{})(tx*DB){

tx=db.getInstance()

tx.Statement.Dest=values

returntx.callbacks.Update().Execute(tx)

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

FirstOrInit

獲取第一條匹配的記錄,或者根據(jù)給定的條件初始化一個實(shí)例(僅支持struct和map)

//FirstOrInitgetsthefirstmatchedrecordorinitializeanewinstancewithgivenconditions(onlyworkswithstructormapconditions)

func(db*DB)FirstOrInit(destinterface{},conds...interface{})(tx*DB){

queryTx:=db.Limit(1).Order(clause.OrderByColumn{

Column:clause.Column{Table:clause.CurrentTable,Name:clause.PrimaryKey},

iftx=queryTx.Find(dest,conds...);tx.RowsAffected==0{

ifc,ok:=tx.Statement.Clauses["WHERE"];ok{

ifwhere,ok:=c.Expression.(clause.Where);ok{

tx.assignInterfacesToValue(where.Exprs)

//initializewithattrs,conds

iflen(tx.Statement.attrs)0{

tx.assignInterfacesToValue(tx.Statement.attrs...)

//initializewithattrs,conds

iflen(tx.Statement.assigns)0{

tx.assignInterfacesToValue(tx.Statement.assigns...)

return

注意,Init和Create的區(qū)別,如果沒有找到,這里會把實(shí)例給初始化,不會存入DB,可以看到RowsAffected==0分支的處理,這里并不會走Create的callback函數(shù)。這里的定位是一個純粹的讀接口。

FirstOrCreate

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

//FirstOrCreategetsthefirstmatchedrecordorcreateanewonewithgivenconditions(onlyworkswithstruct,mapconditions)

func(db*DB)FirstOrCreate(destinterface{},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},

ifresult:=queryTx.Find(dest,conds...);result.Error==nil{

ifresult.RowsAffected==0{

ifc,ok:=result.Statement.Clauses["WHERE"];ok{

ifwhere,ok:=c.Expression.(clause.Where);ok{

result.assignInterfacesToValue(where.Exprs)

//initializewithattrs,conds

iflen(db.Statement.attrs)0{

result.assignInterfacesToValue(db.Statement.attrs...)

//initializewithattrs,conds

iflen(db.Statement.assigns)0{

result.assignInterfacesToValue(db.Statement.assigns...)

returntx.Create(dest)

}elseiflen(db.Statement.assigns)0{

exprs:=tx.Statement.BuildCondition(db.Statement.assigns[0],db.Statement.assigns[1:]...)

assigns:=map[string]interface{}{}

for_,expr:=rangeexprs{

ifeq,ok:=expr.(clause.Eq);ok{

switchcolumn:=eq.Column.(type){

casestring:

assigns[column]=eq.Value

caseclause.Column:

assigns[column.Name]=eq.Value

default:

returntx.Model(dest).Updates(assigns)

}else{

tx.Error=result.Error

returntx

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

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

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

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

不管是否找到記錄,Assign都會將屬性賦值給struct,并將結(jié)果寫回?cái)?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*FROMusersWHEREname='non_existing'ORDERBYidLIMIT1;

//INSERTINTO"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*FROMusersWHEREname='jinzhu'ORDERBYidLIMIT1;

//UPDATEusersSETage=20WHEREid=111;

//user-User{ID:111,Name:"jinzhu",Age:20}

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

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

為什么?

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

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

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

方案二:Upsert

鑒于MySQL提供了ONDUPLICATEKEYUPDATE的能力,我們可以充分利用唯一鍵的約束,來搞定并發(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)

//MERGEINTO"users"USING***WHENNOTMATCHEDTHENINSERT***WHENMATCHEDTHENUPDATESET***;SQLServer

//INSERTINTO`users`***ONDUPLICATEKEYUPDATE***;MySQL

//Updatecolumnstonewvalueon`id`conflict

DB.Clauses(clause.OnConflict{

Columns:[]clause.Column{{Name:"id"}},

DoUpdates:clause.AssignmentColumns([]string{"name","age"}),

}).Create(users)

//MERGEINTO"users"USING***WHENNOTMATCHEDTHENINSERT***WHENMATCHEDTHENUPDATESET"name"="excluded"."name";SQLServer

//INSERTINTO"users"***ONCONFLICT("id")DOUPDATESET"name"="excluded"."name","age"="excluded"."age";PostgreSQL

//INSERTINTO`users`***ONDUPLICATEKEYUPDATE`name`=VALUES(name),`age=VALUES(age);MySQL

這里依賴了GORM的Clauses方法,我們來看一下:

typeInterfaceinterface{

Name()string

Build(Builder)

MergeClause(*Clause)

//AddClauseaddclause

func(stmt*Statement)AddClause(vclause.Interface){

ifoptimizer,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的適配是這樣:

packageclause

typeOnConflictstruct{

Columns[]Column

WhereWhere

TargetWhereWhere

OnConstraintstring

DoNothingbool

DoUpdatesSet

UpdateAllbool

func(OnConflict)Name()string{

return"ONCONFLICT"

//BuildbuildonConflictclause

func(onConflictOnConflict)Build(builderBuilder){

iflen(onConflict.Columns)0{

builder.WriteByte('(')

foridx,column:=rangeonConflict.Columns{

ifidx0{

builder.WriteByte(',')

builder.WriteQuoted(column)

builder.WriteString(`)`)

iflen(onConflict.TargetWhere.Exprs)0{

builder.WriteString("WHERE")

onConflict.TargetWhere.Build(builder)

builder.WriteByte('')

ifonConflict.OnConstraint!=""{

builder.WriteString("ONCONSTRAINT")

builder.WriteString(onConflict.OnConstraint)

builder.W

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論