Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

本記事は連載形式で執筆しています。ここまでの記事は以下の通り
①の記事 ~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などの実装の紹介に移りたいとこですが、我慢して
本連載の目的の一つテストコードの書き方について説明していきます。

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

では!

YuitoSato
ScalaとかRubyとかJSとかの人
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away