🙌

sqlcでトランザクションのロールバックエラーを適切に処理する方法

2024/11/08に公開

背景

sqlcでトランザクションを使うことについて、公式ドキュメントを見ていると、少し気になるところがあったので、自分なりにコードを改善してみました。

https://docs.sqlc.dev/en/stable/howto/transactions.html

公式のコード

func bumpCounter(ctx context.Context, db *sql.DB, queries *tutorial.Queries, id int32) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	qtx := queries.WithTx(tx)
	r, err := qtx.GetRecord(ctx, id)
	if err != nil {
		return err
	}
	if err := qtx.UpdateRecord(ctx, tutorial.UpdateRecordParams{
		ID:      r.ID,
		Counter: r.Counter + 1,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

tx.Rollback()のエラーをキャッチしたい

- func bumpCounter(ctx context.Context, db *sql.DB, queries *tutorial.Queries, id int32) error {
+ func bumpCounter(ctx context.Context, db *sql.DB, queries *tutorial.Queries, id int32) (err error) {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
-	defer tx.Rollback()
+	defer func() {
+		err = errors.Join(err, tx.Rollback())
+	}()
	qtx := queries.WithTx(tx)
	r, err := qtx.GetRecord(ctx, id)
	if err != nil {
		return err
	}
	if err := qtx.UpdateRecord(ctx, tutorial.UpdateRecordParams{
		ID:      r.ID,
		Counter: r.Counter + 1,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

tx.Rollback()のエラーをキャッチするため、名前付き戻り値を採用し、erros.Join()を使って、tx.Rollback()のエラーを返すようにしました。
しかしこのままでは、正常にコミットされても、tx.Rollback()が実行されてしまい、以下のようなエラーが発生します。

sql: transaction has already been committed or rolled back

DB操作が失敗した時にのみ、tx.Rollback()を実行する

func bumpCounter(ctx context.Context, db *sql.DB, queries *tutorial.Queries, id int32) (err error) {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
-	defer func() {
-		err = errors.Join(err, tx.Rollback())
-	}()
	qtx := queries.WithTx(tx)
	r, err := qtx.GetRecord(ctx, id)
	if err != nil {
-		return err
+		return errors.Join(err, tx.Rollback())
	}
	if err := qtx.UpdateRecord(ctx, tutorial.UpdateRecordParams{
		ID:      r.ID,
		Counter: r.Counter + 1,
	}); err != nil {
-		return err
+		return errors.Join(err, tx.Rollback())
	}
	return tx.Commit()
}

上記のようにすることで、tx.Rollback()が実行され、ロールバック時のエラーもキャッチできます。

最後に

多くのデータベースシステムでは、既にコミットされたトランザクションに対して Rollback() を呼び出しても、エラーは発生せず、単に無視されます。

とのことなので、別に気にしなくても良いのかもしれません。

Discussion