golangでrepository層の実装方法を考える
January 27, 2021
golangでDDDっぽくrepository層を作成していたが、大きくなってきてリファクタリングが必要になってきたので色々考える。 repository層に求めるものは以下の通り。
- SQLは完全にrepository層の中で簡潔させたい
- transactionは外に出して、外部でRollbackをする
使っているのはsqlx想定で、もともとのrepository層は以下のような感じ。
type Repository struct {
db *sqlx.DB
}
func (r *Repository) GetUser(id UserID) (User, error)
func (r *Repository) GetTodo(id TodoID) (Todo, error)
func (r *Repository) GetTodosFromUserID(id UserID) ([]Todo, error)
色々調べてた感じだと、やはりモデルごとにrepository構造体を分ける実装が多かった。 確かに、その方が分かりやすいもんね。
type Repository struct
func (r *Repository) User() *UserRepository
func (r *Repository) Todo() *TodoRepository
type UserRepository struct
func (u *UserRepository) Get(id UserID) (User, error)
type TodoRepository struct
func (t *TodoRepository) Get(id TodoID) (Todo, error)
func (t *TodoRepository) SelectFromUserID(id UserID) ([]Todo, error)
まあ、ここまではいいとしてこの時にtransactionをどうするか問題が残る。 トランザクションをどんな感じで取り出すかを考えていく。
transaction用のメソッドを分ける
実装するとこんな感じかな。
type UserRepository struct
func (u *UserRepository) Get(id UserID) (User, error)
func (u *UserRepository) Get(tx *sqlx.Tx, id UserID) (User, error)
メリット:
- 単純明快分かりやすい
デメリット:
- コード量が必要
- コピペが必要なので修正箇所が分散される
transaction用にrepositoryを分ける
transactionを使うようにrepositoryを分ける。
type UserTransactionRepository struct
func NewUserTransactionRepository(tx *sqlx.Tx) *UserTransactionRepository
func (u *UserTransactionRepository) Get(id UserID) (User, error)
メリット:
- これもかなり分かりやすい
デメリット:
- コード量が必要
- これも修正箇所が分散される
この方法もtransaction用のメソッドを分ける方法とあんまり変わらないかなあ。
*sqlx.DB
と*sqlx.Tx
をinterfaceで包み込む
これは、UserRepositoryに今使っているdbのコネクションがトランザクションなのかどうかを知らせない方法。
// これは*sqlx.DBのメソッドを羅列していく
type DBConnection interface
type UserRepository struct {
db DBConnection
}
func NewUserRepository(db DBConnection) *UserRepository
func (u *UserRepository) Get(id UserID) (User, error)
このように書くことによって、NewUserRepository
をするときに*sqlx.DB
を入れても*sqlx.Tx
を入れても使える
メリット:
- UserRepositoryは考慮することが無くなる
- transaction付きのコードでも分ける必要が無いので修正箇所が一か所になる
デメリット:
- DBConnectionのメソッドの羅列が面倒くさい
- repositoryの中でトランザクションが使えない
まとめ
僕は一番最後の*sqlx.DB
と*sqlx.Tx
をinterfaceで包み込むものがいいかなと思いました。
正直DBConnectionのメソッドの羅列はめんどくさいのですが、やはりSQL周りのコードが集約されるのはかなりうれしいポイントかなと思います。
後はtransactionもinterfaceに包み込んでしまうと結構色々な便利メソッドが生やせる点も気に入ってます。
func (r *Repository) Begin() Transaction {
tx, _ := r.db.Beginx()
return &transaction{tx}
}
type Transaction interface {
DBConnection // 先ほどの*sqlx.DBのメソッドを羅列したもの
Commit() error
Rollback() error
RollbackAndWrapError(err error) error
}
var _ Transaction = (*transaction).(nil)
type transaction sturct {
*sqlx.Tx
}
func (t *transaction) RollbackAndWrapError(err error) error {
rErr := t.Rollback()
if err != nil {
return fmt.Errorf("failed to rollback[%+v]: %w", rErr, err)
}
return err
}
// 使い方
repository := NewRepository()
tx := repository.Begin()
userRepository := NewUserRepository(tx)
err := userRepository.Get()
if err != nil {
tx.Rollback()
return
}
tx.Commit()