🔍

Gin の初期化時の挙動について

2023/08/31に公開

Ginを使用している中で初期化まわりの挙動でつまづいたので、備忘録として記したいと思います。

背景

私が携わっているプロジェクトでは、GinとGORMを使用したDDDおよびクリーンアーキテクチャを使用しており、以下のようなディレクトリ構造となっています。

Router層

HTTP リクエストのルーティングを担当します。具体的なURLパターンとそれに対応する Usecase層での処理をマッピングする役割を果たします。

Usecase層

アプリケーションの主要なビジネスロジックを処理します。必要に応じて、Repository層を通じてデータ操作を行います。

Repository層

データアクセスの抽象化を提供します。DB操作のためのインターフェースを定義し、Usecase層はこのインターフェースを通じて DB操作を行うことができます。

Dao層

この層は Repository層で抽象化されたインターフェースの具体的な実装を担当し、実際の DB操作を行います。

私たちのプロジェクトではもともと、GORMが提供するトランザクションメソッドをDaoの中で使用していました。

Router層
func Init(db *gorm.DB) {
	r = gin.New()
	internalHogeUsecase := usecase.NewHogeUsecase(db)
	internalHogeRoute := r.Group("/hoge")
	internalHogeRoute.GET("", internalHogeUsecase.Fuga)
}
Usecase層
type HogeUsecase struct {
	db                *gorm.DB
	repository        repository.HogeRepository
}

func NewHogeUsecase(db *gorm.DB) HogeUsecase {
	repository := dao.NewHogeDao(db)
	return HogeUsecase{db, repository}
}

func (u HogeUsecase) Create(context *gin.Context) {
	id := context.Param("id")
	fuga, err := u.repository.Fuga(id);
	if err != nil {
		return
	}

	context.Status(http.StatusOK, fuga)
}
Repository層
type HogeRepository interface {
	Fuga(id) (model.Fuga, error)
}
Dao層
type HogeDao struct {
	db                *gorm.DB
}

func NewHogeDao(db *gorm.DB) repository.HogeRepository {
	return &HogeDao{db}
}

func (d HogeDao) Fuga(id int) (model.Fuga, error) {
	var fuga model.Fuga
	err := d.db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&fuga).Error; err != nil {
			return errors.Wrap(err, "DB Error")
		}
		return nil
	})
	if err != nil {
		return model.Fuga{}, err
	}

	return fuga, nil
}

このように、Daoでトランザクションを貼ることでエラー発生時のロールバックを行っていましたが、これではメソッドごとにいちいちトランザクションを貼る必要がありました。

そこで、もっとスマートにトランザクションを貼るため、Usecaseのインスタンス作成時にトランザクションインスタンスを渡し、エラーが出たらロールバックをしたいと考えたのです。

事象

スマートにトランザクションを貼るため、リクエスト毎にトランザクションインスタンスを作成し、NewHogeUsecase関数にそのトランザクションを渡すようにしました。

Router層
func Init(db *gorm.DB) {
	r = gin.New()
	tx := db.Begin()          // トランザクションインスタンス作成
	internalHogeUsecase := usecase.NewHogeUsecase(tx)          // 渡す
	internalHogeRoute := r.Group("/hoge")
	internalHogeRoute.GET("", internalHogeUsecase.Fuga)
}

しかし、サーバーエラーが発生してしまいます。

ログ
Error #01: DB Error: invalid transaction
Error #02: DB Error: invalid transaction

問題の所在

なぜこのような事象が起こるのでしょうか。私はdb.Begin()を実行しトランザクションを開始し、NewHogeUsecaseに渡していますが、トランザクションは失敗しています。

このエラーは2回目の実行以降で発生するため、2回目以降において、作成されたトランザクションインスタンスが使用されていないようです。

NewHogeUsecaseに渡すものを追加して検証します。

Router層
func Init(db *gorm.DB) {
	r = gin.New()
	now := time.Now()         //追加
	internalHogeUsecase := usecase.NewHogeUsecase(db, now)
	internalHogeRoute := r.Group("/hoge")
	internalHogeRoute.GET("", internalHogeUsecase.Fuga)
}
Usecase層
type HogeUsecase struct {
	db                *gorm.DB
	repository        repository.HogeRepository
	now               time.Time          // 追加
}

func NewHogeUsecase(db *gorm.DB, now time.Time) HogeUsecase {
	repository := dao.NewHogeDao(db)
	return HogeUsecase{db, repository, now}
}

func (u HogeUsecase) Create(context *gin.Context) {
	fmt.Println(u.now)          // リクエスト毎に変化しているかどうかログで確認
	id := context.Param("id")
	fuga, err := u.repository.Fuga(id);
	if err != nil {
		return
	}

	context.Status(http.StatusOK, fuga)
}

リクエストしてみます。

ログ
// 初回実行
2023-08-31 01:16:04 time.Date(2023, time.August, 31, 1, 8, 18, 920332423, time.Local)

// 2回目
2023-08-31 01:20:11 time.Date(2023, time.August, 31, 1, 8, 18, 920332423, time.Local)

あれ、実行時間は変わっているのにログで吐き出されている時間は変わっていないですね。

原因

実行時間の変化にも関わらず、ログで吐き出されている時間に変更がないのはなぜでしょうか。
NewHogeUsecaseでログを吐き出してみます。

Usecase層
type HogeUsecase struct {
	db                *gorm.DB
	repository        repository.HogeRepository
}

func NewHogeUsecase(db *gorm.DB) HogeUsecase {
	fmt.Printf("実行されていますか?")
	repository := dao.NewHogeDao(db)
	return HogeUsecase{db, repository}
}
サーバー起動時ログ
2023-08-31 01:36:27 [GIN-debug] GET    ...
2023-08-31 01:36:27 実行されていますか?[GIN-debug] GET   ...
2023-08-31 01:36:27 [GIN-debug] GET   ...

このサーバー起動時の1回目のみ、このログは吐き出されました。

つまり、NewHogeUsecaseは初回のみ実行されていることがわかります。Init関数内で生成されるオブジェクトのインスタンス生成は、Init関数が初回にしか実行されないのと同様、一度しか行われないのです。

まとめ

このように、Ginを扱う際には初期化時の挙動に注意する必要があります。リクエスト毎に変わる可能性のあるオブジェクトや、リクエストの内容に基づいて動的に生成したいオブジェクトをハンドラ関数やミドルウェアで扱いたい際は、インスタンスの生成が一度しか行われないことに留意してください。そのようなオブジェクトは初期化時に直接渡すのではなく、必要に応じてその都度取得するような設計にするのがよいと思います。

おまけ

GORMを使用したトランザクションの貼り方

https://gorm.io/ja_JP/docs/transactions.html#トランザクション

https://articles.wesionary.team/implement-database-transactions-with-repository-pattern-golang-gin-and-gorm-application-907517fd0743

Discussion