クリーンアーキテクチャとDBトランザクション
依存例service -> repository
repository層の処理はaddUserとかupdateUserとか単一リソースに対する処理になる
service層はrepositoryを使ってリソースに対して処理を行う
トランザクションは複数のrepositoryにまたがる
問題点
サービス層はDBの具体的な実装を知らないため、db.Beginなどを直接呼び出すことはできない
repositoryにBeginやRollbackを生やして呼び出すこともできるが、serviceの処理が冗長になる
解決策
DoInTx(f func(){})のように、トランザクション内で実行する関数を受け取る関数をrepositoryに生やす
contextpackage を使用してtxの値を複数関数内で受け渡す
code:repository.go
type ctxKey string
var txCtxKey ctxKey = "transaction_context_key"
func (repo *Repository) DoInTx(f func(ctx context.Context) error) error {
tx, err := repo.DB.Begin()
if err != nil {
return err
}
ctx := context.WithValue(context.Background(), txCtxKey, tx)
if err := f(ctx); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
return nil
}
// repoからDBを触るときは基本的にこのヘルパーを介してアクセスする
func (repo *Repository) exec(ctx context.Context, query string, args ...interface{}) (*sql.Row, error) {
tx, ok := ctx.Value(txCtxKey).(*sql.Tx)
if ok {
return tx.Exec(query, args...)
}
return repo.DB.Exec(query, args...)
}
code:service.go
func (service *Service) CreateXXX(field string) error {
if err := service.Repo.DoInTx(func(ctx context.Context) error {
if err := service.Repo.CreateXXX(ctx); err != nil {
return err
}
if err := service.Repo.CreateYYY(ctx); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
ref