共通処理を呼び出してテーブル更新するAPIをGo言語で実装していたときに、共通処理含めてロールバックする方法を作りました。
GORMガイドでは、トランザクションの説明はあるものの、処理をまたいだロールバックについては記載されていないようなので、記事としてまとめてみました。
環境
Golang 1.14.4
例題
図書館のシステムに、
「利用者が本を借りたとき、利用者の所持数を加算し、書架テーブルの貸出中フラグを1に更新する」処理があったとする。
この処理をコードで表すと以下の通り。
func bookRent() error {
// リクエスト情報を受け取る。今回は割愛して固定値で指定する。
userID := 1
bookID := 23
// DB接続用の情報を設定する。
DBMS := “mysql”
CONNECT := “root:mysql@tcp(localhost:3306)/testDB”
db, err := gorm.Open(DBMS,CONNECT)
if err != nil {
return err.ERROR()
}
// セッションを開始する。
tx := db.Begin()
// 予期せぬエラー時のロールバックを行う。
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}
// 利用者の貸し出し冊数に加算する。
var user model.User
if err := tx.Model(&user).Where(“user_id = ? AND delete_flg = 0”, userID).Update(“rental_count”, gorm.Expr(“rental_count + 1”)).Error; err != nil {
tx.Rollback()
return err.ERROR
}
// 蔵書に対して、貸し出し中フラグを「1:貸し出し中」に更新する。
var liblary model.Library
if err := tx.Model(&liblary).Where(“book_id = ? AND delete_flg = 0”, bookID).Update(“rental_flg”, 1).Error; err != nil {
tx.Rollback()
return err.ERROR
}
// すべての更新処理が終わったら、コミットを行う。
tx.Commit()
return nil
}
この処理に対し「貸出中フラグを更新する処理を共通化し、共通化処理を呼び出す形で実装したい」という課題が出た。
この時、正常にロールバックが機能する実装方法を考える。
解決方法
共通処理の引数に、セッション情報を持たせる。
// rentalBoook は、対象となった本を一括で貸し出し中に更新する共通処理です。
func rentalBook(tx *gorm.DB, bookIDs ...int) error {
// 蔵書に対して、貸し出し中フラグを「1:貸し出し中」に更新する。
var liblary model.Library
if err := tx.Model(&liblary).Where(“book_id IN (?) AND delete_flg = 0”, bookIDs).Update(“rental_flg”, 1).Error; err != nil {
tx.Rollback()
return err.ERROR
}
return nil
}
もともとの処理であるbookRentは、以下のように修正する。(変更箇所近辺のみ抜粋)
// セッションを開始する。
tx := db.Begin()
// 予期せぬエラー時のロールバックを行う。
defer func() {
if r := recover(); r != nil {
tx.Rollback()
return err.ERROR()
}
}
// 利用者の貸し出し冊数に加算する。
var user model.User
if err := tx.Model(&user).Where(“user_id = ? AND delete_flg = 0”, userID).Update(“rental_count”, gorm.Expr(“rental_count + 1”)).Error; err != nil {
tx.Rollback()
return err.ERROR()
}
// 共通処理を呼び出して、貸し出しフラグを「1:貸し出し中」に更新する。
bookErr := rentalBook(tx,[]int{bookID}...)
if bookErr != nil {
// 共通処理側でロールバックしているので、こちらではロールバックしない。
return bookErr.ERROR()
}
// すべての更新処理が終わったら、コミットを行う。
tx.Commit()
共通処理を呼び出した際のロールバックについては、呼び出し元の処理で行うか、共通処理側でやるかという問題があると思います。
共通処理で問題が発生し次第ロールバックしたほうが安全だと思うので、上記の例ではそう記載しています。