はじめに
本記事は連載形式で執筆しています。ここまでの記事は以下の通り
①の記事 ~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のトランザクションは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)
}
}
いかかでしたでしょうか?
次回からはこのままSlickやMemchachedクライアントのShadeなどの実装の紹介に移りたいとこですが、我慢して
本連載の目的の一つテストコードの書き方について説明していきます。
では!