scalaのデータベースライブラリとしてslickを業務で使ってみようと試していたが、ドランザクション管理に制限があり、最終的に取りやめることにした。調べたこと、試したことを備忘録的に残しておこうと思う。
slick version : 3.3.0
slickの基本
slickではクエリに相当するDBIOAction
というものをまず生成する。以下の例だとinsert
やupdate
がDBIOAction
に当たる。また、insert
とupdate
を合わせたaction
もDBIOAction
に当たる。
次に、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
ここには以下のようなことが書いてある。
-
DBIOAction
にtransactionally
をつけるとトランザクション対象になり、SQL実行結果に基づいて、自動的に全体がcommit / rollbackされる -
DBIOAction
にwithPinnedSession
をつけると同一のセッションを使いまわして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を使うやり方
他にやり方はないのかと、ググっていると以下のスライドを発見した。
これを参考に、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の並列処理などでは非常に有益なライブラリだと思うので、次のシステムで改めて検討したい。