Go言語のトランザクション処理を共通化する関数を作る

Go言語

今回は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
}

他の言語同様にBeginRollbackCommitを実行させていきます。

このCommitRollbackを最後に必ず実行させたいので、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で共通化してスッキリさせています。

トランザクション処理を共通化する

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処理にレビューを集中することができます。

ぎゅう
WEBエンジニア
渋谷でWEBエンジニアとして働く。
LaravelとVue.jsをよく取り扱い、誰でも仕様が伝わるコードを書くことを得意とする。
先輩だろうがプルリクにコメントをして、リファクタしまくる仕様伝わるコード書くマン
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次
閉じる