scalaのデータベースライブラリとしてslickを業務で使ってみようと試していたが、ドランザクション管理に制限があり、最終的に取りやめることにした。調べたこと、試したことを備忘録的に残しておこうと思う。

slick version : 3.3.0

slickの基本

slickではクエリに相当するDBIOActionというものをまず生成する。以下の例だとinsertupdateDBIOActionに当たる。また、insertupdateを合わせたactionDBIOActionに当たる。
次に、db.run()でアクションを実行することにより、実際にSQLが実行される。

val insert = Hoge.map(h => (h.item1, h.item2)).++(Seq((11, 12), (21, 22))
val update = Hoge.filter(_.item1 === 11).map(h => (h.item2)).update(99)
val action = insert andThen update

val future = db.run(action.transactionally)
val result = Await.result(future, Duration.Inf)

この辺の基本的な使い方は以下のドキュメントを参照のこと。
http://krrrr38.github.io/slick-doc-ja/v3.0.out/slick-doc-ja+3.0.html

やりたいこと

トランザクション管理と書いたが、やりたいことは具体的に以下の2点である

(A) 複数のクエリを一つのトランザクションで実行し、SQLエラーが発生したらロールバックする
(B) 一つのトランザクションの中で、クエリ実行->業務ロジック->結果次第で全体をロールバックできるようにする

トランザクション管理に関する公式ドキュメント

公式ドキュメントには以下の記述がある。
http://slick.lightbend.com/doc/3.0.0/dbio.html#transactions-and-pinned-sessions

ここには以下のようなことが書いてある。

  1. DBIOActiontransactionallyをつけるとトランザクション対象になり、SQL実行結果に基づいて、自動的に全体がcommit / rollbackされる
  2. DBIOActionwithPinnedSessionをつけると同一のセッションを使いまわしてDB外で待ち合わせできる

どうやら、ドキュメントをみる限り、やりたいこと(A)は1.で、やりたいこと(B)は2.で実現できるようだ。

(A) 複数のクエリを一つのトランザクションで実行し、SQLエラーが発生したらロールバックする

特に工夫しなくても、普通にやっていれば実現できる。
ポイントはtransactionallyをつけるところぐらい。

val insert = Hoge.map(h => (h.item1, h.item2)).++(Seq((11, 12), (21, 22))
val update = Hoge.filter(_.item1 === 11).map(h => (h.item2)).update(99)
val action = insert andThen update

Await.result(db.run(action.transactionally), Duration.inf)

ただし制限がある。
一つ目は、上記の例だと、insertとupdateの間にDBにクエリを投げる以外の別の業務ロジックの実行を行うことができないことだ。

二つ目は、例えば以下のように二つのDBIOActionをそれぞれ別々に実行すると、これらは別のトランザクションになってしまうことだ。なのでinsertは成功してupdateがエラーになったとすると、insertの方はcommitされてしまう。したがって、一連のクエリは一つのDBIOActionにまとめる必要がある。

val insert = Hoge.map(h => (h.item1, h.item2)).++(Seq((11, 12), (21, 22))
Await.result(db.run(insert.transactionally), Duration.inf)

val update = Hoge.filter(_.item1 === 11).map(h => (h.item2)).update(99)
Await.result(db.run(update.transactionally), Duration.inf)

(B) 一つのトランザクションの中で、クエリ実行->業務ロジック->場合によって全体をロールバックできるようにする

withPinnedSessionを使う方法

ドキュメントをみる限り、withPinnedSessionを使えば実現できそうである。

まずはこんな感じのコードを試してみた。

val insert = Hoge.map(h => (h.item1, h.item2)).++(Seq((11, 12), (21, 22)).withPinnedSession
Await.result(db.run(insert), Duration.Inf)

// 業務ロジックを実行。その結果エラーになったという感じ
val error = true

if (error) {
  val rollback = SimpleDBIO(_.connection.rollback()).withPinnedSession
  Await.result(db.run(rollback), Duration.Inf)
}

これだと、以下のようなエラーが発生する。

autoCommit有効時に、明示的なロールバックはできません。
org.postgresql.util.PSQLException: autoCommit有効時に、明示的なロールバックはできません。

どうやら、auto-commitをfalseにする必要があるようだ。

ドキュメントに「Slickで利用出来ない機能を使うためにJDBCのレベルを落とすには、SimpleDBIOアクションを用いれば良い」と書いてあるので、以下を追加してみた。

val autoCommitFalse = SimpleDBIO(_.connection.setAutoCommit(false)).withPinnedSession
Await.result(db.run(autoCommitFalse), Duration.Inf)

結果は全く変わらず。
どうやら、withPinnedSessionをつけているにも関わらず、別のDBIOActionだと別セッションになってしまうことが原因のようだが、色々と試してみたものの結局解決することができなかった。(この方向は断念)

startInTransactionを使うやり方

他にやり方はないのかと、ググっていると以下のスライドを発見した。
undefined

これを参考に、slick v3.3.0に存在する関数を使って実装してみた。

val session = db.createSession().asInstanceOf[BaseSession]
session.conn.setAutoCommit(false)
session.startInTransaction

// insert
val insert = Hoge.map(h => (h.item1, h.item2)).++(Seq((11, 12), (21, 22)).transactionally
Await.result(db.run(insert), Duration.Inf)

session.conn.rollback()
// session.endInTransaction(session.conn.rollback()) // この書き方もダメ
session.close()

このやり方もダメだった。普通にコミットされてしまう。

結論、「一つのトランザクションの中で、クエリ実行->業務ロジック->場合によって全体をロールバックできるようにする」という件は、自分の調査能力では実現できなかった。

※どなたかやり方が分かる方がいらっしゃったら教えてくださいm( )m

結論

自分が調査した範囲ではあるが、slickのトランザクション管理でできることは、以下の(A)だけのようだ。

(A) 複数のクエリを一つのトランザクションで実行し、SQLエラーが発生したらロールバックする
(B) 一つのトランザクションの中で、クエリ実行->業務ロジック->結果次第で全体をロールバックできるようにする

今回、insert->業務ロジック->insertのような要件がもしかしたら発生しうるアプリケーションなので、slickの採用は見送ることにした。

とはいえ、以下のような感じで処理順をきちんと考えて実装すれば、ほとんどのケースで大丈夫だと思う。

  • データを取得する(select)
  • 取得したデータを元に業務ロジックを実行する
  • 問題がなければ、更新処理を一つのDBIOActionにまとめて、transactionallyで実行する
    • 万一SQLエラーが発生したら、更新処理全体がロールバックされる
  • 問題が発生したら、更新処理を行わない

SQLの並列処理などでは非常に有益なライブラリだと思うので、次のシステムで改めて検討したい。