初めに
Go言語でDBトランザクション処理をシンプルに書く方法を模索していた。
有力な方法が見つかったので検討の経緯とその具体例を共有する。
#検討した実装パターン候補
*例コードはGORMを利用している前提。
###実直に書く
tx := db.Begin()
if tx.Error != nil {
return nil, tx.Error
}
// 何らかの処理が失敗
tx.Rollback()
// 成功時
tx.Commit()
→ もっとDRYにしたい
→ Rollback or Commitのコールを忘れそう。。
###トランザクション状態を管理しつつdefer関数内で判断し必要に応じてRollbackをコール
(以下検討アイデアの雰囲気コード)
myTx, err := db.MyBegin() //← このレシーバー関数でBeginして管理トランザクション状態を開始にする
if err != nil {
return err
}
defer myTx.MyFinish() //← トランザクション状態判定して未完了ならRollback
// 任意のDB処理
user := User {
Name: "test name",
}
if err := myTx.tx.Create(&user).Error; err != nil {
return err
}
if err = myTx.MyCommit(); err != nil { //← Commit成功時、管理トランザクション状態を完了にする
return err
}
return nil
→ 少しDRYになった
→ もしMyCommit()を忘れてもトランザクションは必ず終了する
→ でも状態管理せずに済むならそちらの方がシンプルで良さそう。。
###(結論)共通ラッパー処理を用意する
共通ラッパー処理を関数化するアイデアを採用した。
以下は返り値情報を受け取る場合。
// トランザクション実行し情報を受取る
func TransactAndReturnData(db *gorm.DB, txFunc func(*gorm.DB) (interface{}, error)) (data interface{}, err error) {
tx := db.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit().Error
}
}()
data, err = txFunc(tx)
return
}
#具体例
以下の様なテーブルスキーマとそれぞれのCreate関数が有ると仮定する。
type User struct {
ID uint `gorm:"primary_key"`
Name string
}
type Event struct {
ID uint `gorm:"primary_key"`
UserID uint // ← 外部キー
Name string
}
// ユーザー作成
CreateUser(db *gorm.DB, name string) (*User, error)
// ユーザーに紐付くイベント作成
CreateEvent(db *gorm.DB, userID uint, name string) (*Event, error)
以下の様にトランザクションを扱いつつまとめて関数化出来る。
func CreateUserAndEventInTransaction(db *gorm.DB, userName, eventName string) (*User, error) {
data, err := TransactAndReturnData(db, func(tx *gorm.DB) (interface{}, error) {
user, err := CreateUser(tx, userName)
if err != nil {
return nil, err
}
_, errEvent := CreateEvent(tx, user.ID, eventName)
if errEvent != nil {
return nil, errEvent
}
return user, nil
})
if err != nil {
return nil, err
}
// 受け取りたいものをラッパー関数内で返却してキャストすれば良い
return data.(*User), nil
}
ラッパー関数内で漏れなく各処理の結果を返す事だけを守れば安全かつシンプルにトランザクション処理が書ける様になった!
参考
[golangでトランザクション管理を少し楽にするラッパー関数]
(http://tsujitaku50.hatenablog.com/entry/2017/11/01/232707)