為了保持我們的資料能正確的寫入與正確的不寫入,今天我們要來了解一下 transactions 是怎麼運作的,以及如果沒有實作 transactions 會發生怎樣的悲劇,最後提供 gorm 實作 transactions 的範例程式碼給大家參考。
Transactions 資料庫用來處理一連串 SQL queries的方式,用來防止服務需更新多筆資料或多個 table 時,任一筆資料更新失敗而產生的更新不完整狀態出現。被包在 transactions 中的的操作只要有一筆失敗便可以透過 rollback 來將其它已操作的部分復原。
我們使用先前的 table film 做為範例,在同時寫入兩筆資料 filmA
& filmB
,並且在 filmB
的部分刻意少填欄位,來造成其中一次操作錯誤的情況,來觀察有無使用 transactions 的效果。
/*
NoTransactions batch insert transactions example
curl --location --request POST '127.0.0.1/example/notransactions'
*/
func NoTransactions(c *gin.Context) {
filmA := map[string]interface{}{
"name": "瘋狂麥斯",
"category": "科幻",
"length": 180,
}
filmB := map[string]interface{}{
"name": "捍衛任務",
"length": 180,
}
if err := sqlMaster.Table(FilmModel{}.TableName()).Debug().Create(&filmA).Error; err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if err := sqlMaster.Table(FilmModel{}.TableName()).Debug().Create(&filmB).Error; err != nil {
//Field 'category' doesn't have a default value
c.String(http.StatusInternalServerError, err.Error())
return
}
c.String(http.StatusOK, "never reach")
}
在沒有使用 transactions 時,filmA
成功被寫入,filmB
因缺少欄位被拒絕寫入,此時對使用者來說造成不完全成功的狀況。
/*
Transactions batch insert transactions example
curl --location --request POST '127.0.0.1/example/transactions'
*/
func Transactions(c *gin.Context) {
tx := sqlMaster.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
filmA := map[string]interface{}{
"name": "瘋狂麥斯",
"category": "科幻",
"length": 180,
}
filmB := map[string]interface{}{
"name": "捍衛任務",
"length": 180,
}
if err := tx.Table(FilmModel{}.TableName()).Create(filmA).Error; err != nil {
tx.Rollback()
c.String(http.StatusInternalServerError, err.Error())
return
}
if err := tx.Table(FilmModel{}.TableName()).Create(filmB).Error; err != nil {
// Field 'category' doesn't have a default value
tx.Rollback()
c.String(http.StatusInternalServerError, err.Error())
return
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return
}
c.String(http.StatusOK, "never reach")
}
在沒有使用 transactions 時,filmA
判斷可以被寫入,filmB
因缺少欄位被拒絕寫入,且因尚未執行 tx.Commit()
方法,兩個操作皆未被實際執行,故當我們去資料庫觀察資料時,不會看到只有 filmA
被寫入的窘境。特別停醒一下,雖然以範例中 tx.Commit()
未被執行所以我們的queries 沒有被送出,但以相同條件下不 return
,MySQL 依然會拒絕這次 commit 請求,並且在我們err := tx.Commit()
的地方回傳錯誤。
在同一次服務操作請求時,若有需要對 DB進行多筆資料異動,我們務必使用 transactions 將逐筆操作包覆起來,確保這批 SQL queries 是能同時成功或同時失敗的。另外觀察我們的範例程式碼可以注意到,transactions 的使用是對相同 DB 進行包覆,如果是對不同 DB實體操作,甚至是不同類型的 DB 操作 (ex. MySQL + Redis),那 transactions 並無法保護你的服務正確性,在資料分散在不同類型實體時,需要用一些業務邏輯或架構進行完整性的保護。