Input中心のブログ

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()