はじめに
本記事は連載形式で執筆しています。ここまでの記事は以下の通り。
①の記事 ~howとwhatの分離~
トランザクションとは
よくリレーショナル・データベースで使われる言葉で、**「データベースにアクセスする一連の処理のひとまとまり」**を指します。
詳しくはこちら
前回の記事の最後で、
trait Transaction[A]
trait UserRepository {
def find(id: Long): Transaction[Option[User]]
}
このようなコードを書きました。
この記事では上記の通り、idでユーザーのデータを検索して、ユーザーを探してくる一連の処理をトランザクションとみなしています。
コミットとロールバック
リレーショナルデータベースではコミットとロールバックという考えがあります。
ここでは読者の皆さんは上記概念を知っているものとして話を進めます。
データベースのトランザクションをTransactionクラスで表現するならば、Transactionを実行する処理の中でエラーが起きたら自動的にロールバックが起きてほしいですね。
これをどう表現するのかというと、
trait Transaction[A] {
def run: Result[A]
}
sealed trait Result[A]
// ブロッキング
case class SyncResult[A](
value: DomainError \/ A
) extends Result[A]
// ノンブロッキング
case class AsyncResult[A](
value: EitherT[Future, DomainError, A]
) extends Result[A]
// Scalaのエラーハンドリングでよく使うエラーを表現したEnumみたいなアレです。
sealed trait DomainError {
val code: String
val message: String
}
こう表現します。
Transactionのrunメソッドを実行するとトランザクションがコミットされます。
実行中にエラーを検知するとリレーショナル・データベースの実装においては自動的にロールバックが走るように実装クラスを用意します。
エラーハンドリングにはscalazのEitherやEitherTを使用しています。ご容赦ください。
しかし、トランザクションのコミット方法はOR Mapperの実装によって変わりそうです。
なので、
trait TransactionRunner {
def run[A](transaction: Transaction[A]): Result[A]
}
trait Transaction[A] { self =>
def run(implicit runner: TransactionRunner): Result[A] = {
runner.run(self)
}
}
TransactionRunnerというコミット専用のインターフェースを用意します。
OR MapperによってこのRunnerクラスを実装していけばよさそうですね。
トランザクションの変換
トランザクションが内包するオブジェクトはmapによって計算できると便利ですね。
trait Transaction[A] { self =>
def map[B](f: A => B): Transaction[B]
// 中略
}
トランザクションの合成
Repositoryのメソッドは小さく保ち、かつ合成することで大きなトランザクションを構築可能にします。
ここではモナドのflatMapによってトランザクションの合成を表現します。
trait Transaction[A] { self =>
def map[B](f: A => B): Transaction[B]
def flatMap[B](f: A => Transaction[B]): Transaction[B]
// 中略
}
トランザクションモナドの使い方
では実際にServiceクラスを用意して、トランザクションモナドを使用していきます。
// 今回はgoogle guiceを使ってDIしたと仮定。
class UserService @Inject()(
userRepository: UserRepository
)(
implicit runner: TransactionRunner
) {
// runでトランザクションをコミットする
// リレーショナル・データベースによる実装ならロールバックできるように実装クラスを用意する(今後の記事で紹介します)
def create3Times(user: User): Result[Unit] = {
(for {
_ <- userRepository.create(user)
_ <- userRepository.create(user)
_ <- userRepository.create(user)
}).run
}
}
trait UserRepository {
def create(user: User): Transaction[Unit]
}
// おさらい
trait TransactionRunner {
def run[A](transaction: Transaction[A]): Result[A]
}
// おさらい
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)
}
}
どうでしょうか?最終形態のイメージはついてきたでしょうか?
今回はここまでにして、
次の③の記事では実際にScalikejdbcでの実装方法を紹介していきます。
※今回はわりとリレーショナル・データベース寄りのことについて話してしまいました。
もちろんキャッシュなどのデータベースではロールバックはできないのでご理解ください。