0Pricing
Scala for Backend Engineering & Functional Programming · レッスン

トランザクションを組み合わせる

エフェクトを安全に組み合わせます。

「トランザクションを組み合わせる」はCoddyKit上の無料Scala for Backend Engineering & Functional Programmingレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはScala for Backend Engineering & Functional Programming学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。

ConnectionIOはモナド

Doobieの真価は、ConnectionIOがモナドであることです。flatMapやfor内包表記を使って、多数のステートメントを1つの大きなプログラムに組み合わせられます。

この方法で順序付けた処理はすべて、同じトランザクション内の同じ接続で実行されます。

import doobie.implicits._

val program: ConnectionIO[Long] =
  for {
    id <- insertUser("Ada")
    _  <- insertProfile(id)
  } yield id

transactごとに1つのトランザクション

どれだけ多くのステートメントを合成しても、.transactを呼び出すと、全体のConnectionIOが1つのトランザクションになります。

いずれかのステップが失敗すると、Transactorの戦略によってすべてがロールバックされます。すべて成功した場合は、最後に一度だけコミットされます。

val io: IO[Long] = program.transact(xa)

複数ステートメントのアトミックな書き込み

合成されたステートメントは同じトランザクションを共有するため、アトミック性を自然に得られます。ある口座から引き落とし、別の口座に入金する振替は、完全に実行されるか、まったく実行されないかのどちらかになります。

2つ目の更新が例外をスローしても、部分的な状態が残ることはありません。

def transfer(from: Long, to: Long, cents: Int) =
  for {
    _ <- sql"update acct set bal = bal - $cents where id = $from".update.run
    _ <- sql"update acct set bal = bal + $cents where id = $to".update.run
  } yield ()

エラーの発生と処理

ConnectionIOにはMonadErrorインスタンスがあるため、raiseErrorでトランザクションを中断し、ロールバックを発生させられます。

また、handleErrorWithで回復することもできます。ただし、回復しただけでは、それ以前のステートメントが取り消されるわけではありません。トランザクションがロールバックされる必要があります。

import cats.syntax.all._
import doobie.free.connection.{raiseError, pure}

def debit(id: Long, c: Int): ConnectionIO[Unit] =
  sql"update acct set bal = bal - $c where id = $id".update.run.flatMap {
    case 1 => pure(())
    case _ => raiseError(new RuntimeException("no such account"))
  }

トランザクション内の条件分岐

単なるモナドなので、通常のScalaの制御フローが使えます。行を読み取り、その値で分岐し、それに応じて書き込む処理を、すべてアトミックに実行できます。

これにより、ビジネスルールと永続化を一貫した1つの単位に保てます。

for {
  bal <- sql"select bal from acct where id = $id".query[Int].unique
  _   <- if (bal >= amt)
           sql"update acct set bal = bal - $amt where id = $id".update.run
         else raiseError(new RuntimeException("insufficient funds"))
} yield ()

部分ロールバック用のセーブポイント

より細かく制御するために、DoobieはFC(free connection)代数を介してJDBCのSavepointを公開しています。トランザクション全体を中止せず、セーブポイントまでロールバックできます。

これにより、1つの外側のトランザクション内でtry/fallbackパターンを利用できます。

import doobie.free.{connection => FC}

val withSp =
  for {
    sp <- FC.setSavepoint
    _  <- riskyWrite.handleErrorWith(_ => FC.rollback(sp))
  } yield ()

クエリと書き込みの組み合わせ

読み取りと書き込みは同じプログラム内で自由に混在させられます。現在の状態をselectし、Scalaで変更を計算し、それを同じ接続で永続化するといった処理が可能です。

読み取りでは、同じトランザクション内で先に行われたコミット前の変更も確認できます。

for {
  v <- sql"select stock from items where id = $id".query[Int].unique
  _ <- sql"update items set stock = ${v - 1} where id = $id".update.run
  n <- sql"select stock from items where id = $id".query[Int].unique
} yield n

traverseによるプログラムの再利用

プログラムは値なので、catsのtraverseを使ってリストの各要素に同じプログラムを実行し、1つのトランザクション内で順序付けられます。

すべてのinsertがまとめてコミットされるか、1つもコミットされないかのどちらかになります。

import cats.syntax.all._

def saveAll(names: List[String]): ConnectionIO[List[Long]] =
  names.traverse(insertUser)

分離レベル

処理を行う前に接続代数を使えば、プログラム内でトランザクションの分離レベルを設定できます。

SERIALIZABLEのような厳格なレベルは異常を防ぎますが、競合するトランザクションをデータベースが中止することがあります。その場合は再試行してください。

import doobie.free.{connection => FC}
import java.sql.Connection

val serializable =
  FC.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) *> program

シリアライズ失敗時の再試行

再試行はトランザクションではなくエフェクト層で行います。.transactの後にはIOが得られるため、シリアライズエラーが発生した場合はトランザクション全体を再試行できます。

再試行するたびに、新しく独立したトランザクションが開始されます。

def runWithRetry(io: IO[Unit], n: Int): IO[Unit] =
  io.handleErrorWith {
    case _ if n > 0 => runWithRetry(io, n - 1)
    case e          => IO.raiseError(e)
  }

トランザクションを短く保つ

1つのConnectionIOには、アトミックにする必要がある処理だけを組み込んでください。トランザクション内で外部呼び出し(HTTPや時間のかかるCPU処理)を行わないでください。接続とロックを保持してしまいます。

可能であれば読み取りと計算を外で行い、その後、短いトランザクションで書き込んでください。

// good: gather input first, then one short transactional write
val write: ConnectionIO[Int] =
  sql"update users set name = $name where id = $id".update.run

クイックチェック

トランザクションの境界について考えてみてください。

まとめ

ConnectionIOはモナドなので、for内包表記でステートメントを合成すると、.transactごとに1つのアトミックなトランザクションになります。エラーが発生するとすべてがロールバックされ、raiseErrorで意図的に中断できます。

部分ロールバックにはセーブポイントを使い、プログラム内で分離レベルを設定し、エフェクト層でシリアライズ失敗を再試行し、トランザクションを短く保ってください。

よくある質問

「トランザクションを組み合わせる」レッスンは無料ですか?

はい。「トランザクションを組み合わせる」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Scala for Backend Engineering & Functional Programmingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。

「トランザクションを組み合わせる」で何を学びますか?

エフェクトを安全に組み合わせます。 ブラウザで直接実行するハンズオンコードでScala for Backend Engineering & Functional Programmingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Scala for Backend Engineering & Functional Programmingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのScala for Backend Engineering & Functional Programmingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「トランザクションを組み合わせる」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このScala for Backend Engineering & Functional Programmingレッスンでコードを書いて実行できますか?

はい。すべてのScala for Backend Engineering & Functional Programmingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Transactorで接続する
  2. クエリを実行する
  3. 挿入と更新
  4. トランザクションを組み合わせる
← Scala for Backend Engineering & Functional Programmingに戻る