4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Scalaで最強のRepositoryパターンを実装する ~③ScalikeJDBCによる実装~

Last updated at Posted at 2018-04-28

はじめに

本記事は連載形式で執筆しています。ここまでの記事は以下の通り
①の記事 ~howとwhatの分離~
②の記事 ~トランザクションモナド~

ScalikeJDBCとは

ScalikeJDBCはScalaのO/Rマッパーのひとつです。
タイプセーフでかけたり、ドキュメントがしっかりしていたりので個人的に好きです。

公式ドキュメントはこちら

実装方針

まずは前回のおさらいから、

trait Transaction { self =>

  def map[B](f: A => B): Transaction[B]

  def flatMap[B](f: A => Transaction[B]): Transaction[B]

  def run(implicit runner: TransactionRunner): Result[A] = {
    runner.run(self)
  }

}

trait TransactionRunner {

  def run[A](transaction: Transaction[A]): Result[A]

}

trait UserRepository {

  def create(user: User): Transaction[Unit]

}

class UserService @Inject()(
  userRepository: UserRepository
)(
  implicit runner: TransactionRunner
) {

  def create3Times(user: User): Result[Unit] = {
    (for {
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
    }).run
  }

}

このような形になっていました。
今回ScalaikeJDBCで実装していくのはこの、

  • Transaction
  • TransactionRunner
  • UserRepository
    の3つになります。

Transactionの実装

前提として今回の実装はRDBのO/R Mapperの実装です。
なので、Transactionをrun時(=トランザクションをコミット時)にエラーを検知したらロールバックが起きるように実装したいです。

そのためにはまず、そのO/Rマッパーのトランザクション処理の実装を把握する必要があります。

scalikejdbc-cookbook

知らない人は、公式ドキュメントを読んで学んでおきましょう。

ScalikeJDBCのトランザクションはrun時に同じインスタンスのDBSessionを渡していれば同一のトランザクションとみなされます。

これをどう表現していくのかというと、


case class ScalikeJDBCTransaction[A](
  execute: DBSession => DomainError \/ A
) extends Transaction[A] {

  override def map[B](f: A => B): ScalikeJDBCTransaction[B] = {
    val exec = (session: DBSession) => execute(session).map(f)

    ScalikeJDBCTransaction(exec)
  }

  override def flatMap[B](f: A => Transaction[B]): Transaction[B] = {
    val exec = (session: DBSession) => execute(session).map(f).flatMap(_.asInstanceOf[ScalikeJDBCTransaction[B]].execute(session))
    ScalikeJDBCTransaction(exec)
  }

}

こう表現します。

DBSessionを受け取り、エラーハンドリングした何かしらの型を返す(DBSession => DomainError / A)関数を内包したケースクラスとして定義します。

mapやflatMapする場合も、メソッド内で次のScalikeJDBCTransactionで内包すべき関数を作成して、その関数を包むことによって新たなTransactionを生成しています。

最後まで説明しないと、理解しがたい部分もあると思うので先に進みます。

TransactionRunnerの実装

TransactionRunnerの実装はこうです。最終的に合成したトランザクションモナドをコミットするためのメソッドを持ちます。

class ScalikeJDBCTransactionRunner extends TransactionRunner {

  override def run[A](transaction: Transaction[A]): Result[A] = {
    SyncResult {
      DB localTx { session =>
        transaction.asInstanceOf[ScalikeJDBCTransaction[A]].execute(session)
      }
    }
  }

}

Transactionコンパニオンオブジェクト

ScalikeJDBCTransactionの生成を簡単にするために、コンパニオンオブジェクトにファクトリメソッドを定義しておきます。

object ScalikeJDBCTransaction {

  def from[A](execute: DBSession => A): ScalikeJDBCTransaction[A] = {
    val exec = (session: DBSession) => {
      Try {
        execute(session)
      } match {
        case Success(r) => \/-(r)
        // ThrowableをDomainErrorに変換して、Left値に詰めてるだけ。
        case Failure(l) => -\/(DomainError.Unexpected(l))
      }
    }
    ScalikeJDBCTransaction(exec)
  }

}

UserRepositoryの実装

次はUserRepositoryを実装していきます。

class UserRepositoryScalikeJDBC extends UserRepository {

  override def create(user: User): ScalikeJDBCTransaction[Unit] = {
    ScalikeJDBCTransaction.from { session: DBSession =>
      Users.create(
        // 中略
      )(session)
    }.map(_ => ())
  }

}

Users.create ~ のところは以下のDBSessionとユーザー情報を受け取り、ユーザーのレコードを追加するメソッドだと仮定します。
今回は詳しく解説はしません。

使用方法

class UserService @Inject()(
  userRepository: UserRepository
)(
  implicit runner: TransactionRunner
) {

  def create3Times(user: User): Result[Unit] = {
    (for {
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
    }).run
  }

}

DIの設定だけすれば、変わらず上記のような形で作成でき、トランザクションも同一のものとして処理できるはずです。

今回は解説のため、DIやimplicitを使わない場合も置いておきます。

class UserServiceScalikeJDBC {
  
  val userRepository = new UserRepositoryScalikeJDBC
  val transactionRunner = new ScalikeJDBCTransactionRunner

  def create3Times(user: User): SyncResult[Unit] = {
    (for {
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
      _ <- userRepository.create(user)
    }).run(transactionRunner)
  }

}

いかかでしたでしょうか?

次回からはこのままSlickMemchachedクライアントのShadeなどの実装の紹介に移りたいとこですが、我慢して
本連載の目的の一つテストコードの書き方について説明していきます。

④の記事 ~テストコード編~

では!

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?