今回はGo言語でトランザクション処理を共通化する方法を紹介したいと思います。
そのために、まずは基本的なトランザクションの流れを紹介します
目次
Go言語でのトランザクションの流れ
簡単にトランザクションの流れを理解しましょう。
func main() {
// DBに接続
db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
if err != nil {
log.Fatalln(err)
}
// トランザクション開始
tx := db.Begin()
// SQL実行
_, err := tx.Exec("INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "Jason", "Moiron", "jmoiron@jmoiron.net")
// エラーがあればロールバック
if err != nil {
// ロールバック
_ = tx.Rollback()
return
}
// エラーがなければコミット
tx.Commit()
return
}
他の言語同様にBegin
やRollback
、Commit
を実行させていきます。
このCommit
やRollback
を最後に必ず実行させたいので、defer
で最後に実行させるようにします。
func (s Service) DoSomething() (err error) {
// トランザクション開始
tx, err := s.db.Begin()
if err != nil {
return
}
// deferで関数の最後にtx.Rollback()かtx.Commitをする
defer func() {
// errorがあれば、Rollback
if err != nil {
tx.Rollback()
return
}
// errorがなければcommit
err = tx.Commit()
}()
// 実行させたいSQLを実行
if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...
return
}
このようにdefer
を利用することで、関数の最後にRollback()
またはCommit()
を必ず実行させます。
SQLの実行後に毎回Rollback処理を書くとコードも膨むので、deferで共通化してスッキリさせています。
database/sql Tx – detecting Commit or Rollback
Using the database/sql and driver packages and Tx, it is not possible it appears to detect whether a transaction has been committed or rolled-back without attem…
トランザクション処理を共通化する
deferでトランザクションをスッキリさせる方法を理解したところで、
このトランザクション処理を共通化していきます。
関数を作る際に毎回トランザクション処理を書くのではなく、レビューして反映済みのコードを開発者全員で利用して、トランザクション処理の信頼性を高めます。
このトランザクションちゃんとやっているよねと不安にならずに済むので、レビュー負荷も軽減されますね。
では実際に見ていきましょう
package transaction
import(
"gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
)
func Run(ctx context.Context, db *DB, fn func(ctx context.Context, tx *sqlx.Tx) error) (err error) {
tx, err := db.BeginTxx(ctx, nil)
if err != nil {
return err
}
defer func() {
// recover()でpanicをキャッチ。ロールバックしてからpanicにする
if p := recover(); p != nil {
_ = tx.Rollback()
panic(p)
} else if err != nil {
_ = tx.Rollback()
} else {
err = tx.Commit()
}
}()
// 第二引数で渡された関数を実行します。
err = fn(ctx, tx)
return
}
第3引数に注目してください。
func Run(ctx context.Context, db *DB, fn func(ctx context.Context, tx *sqlx.Tx) error) (err error) {
fn func(ctx context.Context, tx *sqlx.Tx) error)
と関数を受け取っています。
この関数を最後に実行させています
// 第二引数で渡された関数を実行します。
err = fn(ctx, tx)
return
これにより、実行させたいSQLを自由に組み込むことができます。
例えば下記だと変数f
に関数を定義して、transaction.Run(ctx, s.db, f)
で共通化したトランザクション処理に渡しています
func (s Service) DoSomething() (err error) {
// transactionで実行させたい処理
f := func(ctx context.Context, tx *sqlx.Tx) error {
if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
}
// トランザクションを共通パッケージで実行
return transaction.Run(ctx, s.db, f)
}
このようにすれば、トランザクションを共通化することができるので、実行するSQL処理にレビューを集中することができます。
コメント