




版權(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)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 砼工程技術(shù)交底
- 2026屆上海市高橋中學(xué)高三上化學(xué)期中學(xué)業(yè)質(zhì)量監(jiān)測模擬試題含解析
- 尿液上皮細(xì)胞臨床解析
- 如何書寫方案匯報(bào)
- 熱控車間動畫講解
- 消化道腫瘤的預(yù)防
- 內(nèi)蒙古烏蘭察布市集寧區(qū)集寧一中2026屆化學(xué)高三上期中達(dá)標(biāo)檢測試題含解析
- 項(xiàng)目履約季度匯報(bào)
- 施工安全管理匯報(bào)
- 唐代楷書教學(xué)講解
- 乳業(yè)公司倉庫管理制度
- 2025-2030中國磁懸浮離心鼓風(fēng)機(jī)行業(yè)市場發(fā)展趨勢與前景展望戰(zhàn)略研究報(bào)告
- 2025年班組長個人職業(yè)素養(yǎng)知識競賽考試題庫500題(含答案)
- 城市污水處理廠運(yùn)行優(yōu)化措施
- 新《職業(yè)病危害工程防護(hù)》考試復(fù)習(xí)題庫(濃縮500題)
- 數(shù)字時代跨文化適應(yīng)機(jī)制-洞察闡釋
- 老年人體頭部有限元建模及碰撞損傷機(jī)制的深度剖析與研究
- 夫妻存款贈與協(xié)議書
- 2025中式烹調(diào)師(初級)理論知識測評試卷(烹飪健康飲食)
- 礦山合作勘探協(xié)議書
- 配貨服務(wù)代理合同協(xié)議
評論
0/150
提交評論