LoginSignup
0
0

[Go言語]deferでトランザクション処理のエラーを返却する

Last updated at Posted at 2023-03-27

やりたいこと

  • defer関数内のerrorの値を、最終的なerrorの返り値として呼び出し元に返却したい
  • 一緒にCommit()とRollback()も上手く捌きたい ≒ 共通化したい

実装例

個人で作成してみた参考レポジトリから、一部抜粋しています。
ファイルを直接参照されたい場合は、こちらからどうぞ。
(if文が続いて読みにくい部分は、各々でリファクタリングしてご参考になればと思います :bow:

tx.go
// トランザクションの共通処理をメソッド化
func (r *Repository) BeginTx(ctx context.Context, db Beginner) (*sql.Tx, func(context.Context, *sql.Tx, error) error, error) {
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return nil, nil, apperror.PROCESS_TRANSACTION_FAILED.Wrap(err, err.Error())
	}
    // commitOrRollback関数を返却する
	return tx, commitOrRollback, nil
}

// コミットの実行・コミット失敗時にロールバックする共通処理を関数化
func commitOrRollback(ctx context.Context, tx *sql.Tx, err error) error {
    // トランザクション処理が成功している場合
	if err == nil {
        // コミットを実行
		if err := tx.Commit(); err != nil {
            // コミット失敗の場合、ロールバックを実施
			if rbErr := tx.Rollback(); rbErr != nil {
                // ロールバック失敗の場合、その失敗時のエラーを返却
				return apperror.PROCESS_TRANSACTION_FAILED.Wrap(err, rbErr.Error())
			}
			err = apperror.PROCESS_TRANSACTION_FAILED.Wrap(err, err.Error())
			log.Printf("[%d]trans: rollbacked sucessfully for handling err: %v",
				appcontext.GetTracdID(ctx), err)
            // ロールバック成功の場合、コミット失敗時のエラーを返却
			return err
		}
        // コミット成功の場合、nilを返却
		return nil
    // トランザクション処理が失敗している場合
	} else {
        // ロールバックを実施
		if rbErr := tx.Rollback(); rbErr != nil {
            // ロールバック失敗の場合、その失敗時のエラーを返却
			return apperror.PROCESS_TRANSACTION_FAILED.Wrap(err, rbErr.Error())
		}
		log.Printf("[%d]trans: rollbacked sucessfully for handling err: %v",
			appcontext.GetTracdID(ctx), err)
        // ロールバック成功の場合、トランザクション処理時のエラーを返却
		return err
	}
}

// トランザクション処理の一例
// 新規ユーザの登録と残高作成を行うトランザクション処理
func (r *Repository) RegisterUserTx(
	ctx context.Context, db Beginner, name string,
// 返り値のerrorをerr変数で定義しておく
) (user *entity.User, balance *entity.Balance, err error) {

    // トランザクションの共通処理を呼び出し、commitOrRollback関数を受け取る
	tx, commitOrRollback, err := r.BeginTx(ctx, db)
	defer func() {
        // defer関数内でcommitOrRollback関数を呼び出す
        // errとは別に、txErr変数を定義・初期化する
        // txErrがnilである場合、errの値は上書かれず、そのまま本メソッド(RegisterUserTx)の呼び出し元に返却される
		if txErr := commitOrRollback(ctx, tx, err); txErr != nil {
            // txErrがnilでない場合、返り値であるerrにtxErrを代入(上書き)する
			err = txErr
            // 上書かれたこのerrの値が、本メソッド(RegisterUserTx)の呼び出し元に返却される
		}
	}()

	user, err = r.CreateUser(ctx, tx, name)
	if err != nil {
        // defer関数へ(errを上書く可能性あり)
		return nil, nil, err
	}

	balance, err = r.CreateBalance(ctx, tx, user.ID)
	if err != nil {
        // defer関数へ(errを上書く可能性あり)
		return nil, nil, err
	}
    // defer関数へ(nilを上書く可能性あり)
	return user, balance, nil
}

参考リポジトリ

参考記事

  • 「defer関数内のerrorの値を、最終的なerrorの返り値として呼び出し元に返却したい」について参考になった記事 :bow:

  • 「一緒にCommit()とRollback()も上手く捌きたい ≒ 共通化したい」について参考になった記事 :bow:

追記

  • commitOrRollback関数のリファクタ版を追記
// commitOrRollback handles committing a transaction or rolling it back in case of an error.
func commitOrRollback(ctx context.Context, tx *sql.Tx, err error) error {
	if err != nil {
		return rollbackTransaction(ctx, tx, err)
	}
	return commitTransaction(ctx, tx)
}

// rollbackTransaction handles the rollback of a transaction and logs the appropriate message.
func rollbackTransaction(ctx context.Context, tx *sql.Tx, err error) error {
	if rbErr := tx.Rollback(); rbErr != nil {
		return apperror.PROCESS_TRANSACTION_FAILED.Wrap(err, rbErr.Error())
	}
	log.Printf("[%d]trans: rollbacked sucessfully for handling err: %v",
		appcontext.GetTracdID(ctx), err)
	return err
}

// commitTransaction handles the commit of a transaction and logs the appropriate message.
func commitTransaction(ctx context.Context, tx *sql.Tx) error {
	if err := tx.Commit(); err != nil {
		return rollbackTransaction(ctx, tx, err)
	}
	return nil
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0