iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 16
0

為了保持我們的資料能正確的寫入正確的不寫入,今天我們要來了解一下 transactions 是怎麼運作的,以及如果沒有實作 transactions 會發生怎樣的悲劇,最後提供 gorm 實作 transactions 的範例程式碼給大家參考。

Transactions 資料庫用來處理一連串 SQL queries的方式,用來防止服務需更新多筆資料或多個 table 時,任一筆資料更新失敗而產生的更新不完整狀態出現。被包在 transactions 中的的操作只要有一筆失敗便可以透過 rollback 來將其它已操作的部分復原。

Example

下載範例

我們使用先前的 table film 做為範例,在同時寫入兩筆資料 filmA & filmB,並且在 filmB 的部分刻意少填欄位,來造成其中一次操作錯誤的情況,來觀察有無使用 transactions 的效果。

No 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

/*
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 並無法保護你的服務正確性,在資料分散在不同類型實體時,需要用一些業務邏輯或架構進行完整性的保護。


上一篇
Day15 Http Server & Gorm
下一篇
Day17 Transactions (Redis)
系列文
Go Distributed & Go Consistently30

尚未有邦友留言

立即登入留言