Go言語でトランザクション制御のラッパー関数を作った話
こんにちは。エンジニアの島袋です。ブログ初投稿になります。
今回はGo言語でトランザクション制御のためのラッパー関数を作った話をします。
なぜラッパー関数が必要になったのか
最近はginを使ってAPIを実装しているのですが、ginにはトランザクション制御のためのミドルウェアがなく、自作する必要がありました。
(手動でbegin, commit( or rollback)を書くのはさすがにつらいので。。。)
ググったところ、違うWAFですがechoのサンプルリポジトリのeurie-inc / echo-sampleが参考になりそうでした。
以下はecho-sample/middleware/transaction.go からの抜粋です。
func TransactionHandler(db *dbr.Session) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return echo.HandlerFunc(func(c echo.Context) error { tx, _ := db.Begin() c.Set(TxKey, tx) if err := next(c); err != nil { tx.Rollback() logrus.Debug("Transction Rollback: ", err) return err } logrus.Debug("Transaction Commit") tx.Commit() return nil }) } }
上記はミドルウェアの中でトランザクションを開始し、httpハンドラ本体からerrorが帰ってきたらrollbackを実行、errorが帰ってこなければcommitするという実装になっているようです。
これをパクって真似してginのミドルウェアに移植すればよさそうです。
(ところで、筆者はつい最近までSAStruts(のカスタム)で仕事をしていたのですが、GoのWAFでいうMiddlewareってSAStrutsでいうところのInterceptorなんですよね。やらせる仕事は同じなのに言語やフレームワークで用語が異なるのってちょっとめんどくさ混乱しますね。)
2,3行書き換えれば終わりじゃん、そう考えていた時期が私にもありました
ほぼコピペでいけるじゃないですかと、楽勝ムード漂っていたときにあることに気づきました。
type HandlerFunc func(*Context)
上記はginのHandlerFunc型の関数シグネチャです。
本体処理のハンドラも、middlewareも上記のシグネチャを満たす必要があります。
ええ、返り値がないんです。よって、errorをreturnしてmiddlewareになんらかの処理をさせるということができません。
/(^o^)\
error発生時にpanicにすればmiddlewareの中でrecoverで拾うことは可能ですが、panicを多用するのってgo言語の作法的にどうなんですかという問題があります。
ラッパー関数という手段
さらにググったところ、stackoverflowのdatabase/sql Tx - detecting Commit or Rollbackという投稿が見つかりました。
以下は回答者のコードの抜粋です。
func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) { tx, err := db.Begin() if err != nil { return } defer func() { if p := recover(); p != nil { switch p := p.(type) { case error: err = p default: err = fmt.Errorf("%s", p) } } if err != nil { tx.Rollback() return } err = tx.Commit() }() return txFunc(tx) }
実行したい関数を引数txFuncとして受け取り、トランザクションを開始してからtxFuncを実行。
txFuncの中でerror、もしくはpanicが起こったらdeferのところでrecoverを使って拾い、rollbackを実行、問題なければcommitという流れのようです。
これはいいですね。middlewareでトランザクション制御を行う場合、トランザクション境界がリクエストの開始〜終了となりますが、ラッパー関数方式の場合、トランザクション境界を柔軟に変更することが可能です。
gin用に書き換えてみた
func Transact(sess *dbr.Session, c *gin.Context, txFunc func(*dbr.Tx, *gin.Context) error) (err error) { tx, err := sess.Begin() if err != nil { return } defer func() { if p := recover(); p != nil { switch p := p.(type) { case error: err = p default: err = fmt.Errorf("%s", p) } } if err != nil { tx.Rollback() return } err = tx.Commit() }() return txFunc(tx, c) }
txFuncの中でginの*Contextが参照できるように書き換えてみました。
また、筆者はORマッパーとしてdbrを利用しているので、database/sqlを使っている部分をdbr用に書き換えてあります。
(dbrの説明はdbr – Go 言語 O/R Mapper の紹介が詳しいです。)
このラッパー関数は以下のように使うことができます。
func hoge(c *gin.Context) { sess := conn.NewSession(nil) err := Transact(sess, c, func(tx *dbr.Tx, c *gin.Context) error { // なんらかの処理 }) if err != nil { //エラー処理 } }
とりあえずトランザクション制御できるようになった
上記のラッパーを使ってとりあえずはトランザクション制御ができるようになりました。
go言語、書いてて楽しいのですが、WAFは薄いものが多いため、結構足回りの機能を自作する必要がある感じです。
(まあ今回はほぼコピペだったわけですが。。。)
このあたりの知見がもっと共有されるようになると嬉しいですねー
コメント