オニオンアーキテクチャやクリーンアーキテクチャなど、関心ごとのレイヤーに分離した実装をするとき、トランザクションをどのレイヤーで管理するかは悩みの種だと思います。
ユースケース層でトランザクションを管理する
個人的な推しは「ユースケース層でトランザクションを管理する」です。リポジトリ層ではユースケース層から渡されるトランザクションを利用してデータアクセスを試みます。こうしたい理由は1つのユースケースが複数のデータに渡った更新制御をすることが少なくないからです。また、この方法ではユースケース層がトランザクションに依存してしまう(依存が逆転してしまう)のですが、実装はシンプルになります。
例えば以下は、SNSで新規投稿をする際に記事とタグを合わせて作成するケースです。
func (a *articleUseCase) CreateArticle(ctx context.Context) error { tx, err := a.db.Begin() if err != nil { return err } defer func() { // トランザクション処理が失敗した場合、DBをロールバックしてエラーを返す if err != nil { tx.Rollback() } }() article, err := domain.NewArticleToCreate(...) if err != nil { return ... } err = a.articleRepository.CreateArticleWithTx(ctx, tx, article) if err != nil { return ... } tag, err := domain.NewTagToCreate(...) if err != nil { return ... } err = a.tagRepository.CreateTagWithTx(ctx, tx, tag) if err != nil { return ... } err = tx.Commit() if err != nil { return err } return nil }
実際の実装では、トランザクションをctxとして渡せるように抽象化しますが、今回はイメージしやすいのでユースケース層でそのままトランザクションオブジェクトを取り扱っています。
リポジトリ層でトランザクションを管理するとどうなるか
シンプルなユースケース(単一のテーブルを更新するようなケース)ではリポジトリ層でトランザクションを管理しても問題ないと思います。しかし、そうではない場合、データの一貫性を管理したいためにロジックがリポジトリ層に書かれてしまい、ユースケース層がスカスカになってしまいます。「シンプルなユースケースのみリポジトリ層でトランザクションを管理する」とするのもありかもしれませんが、書き方に統一感がなくなってしまうので「トランザクションはユースケース層で管理する」と決めてしまう方がいいかなと考えています。