2012-01-14

Play framework を扱う案件に最近携わっていて、いろいろとドキュメントにないノウハウが溜まりつつあるのでここらでメモ書きしておきます。


Play framework とトランザクション

Play framework では、HTTP リクエストはもちろんのこと、非同期ジョブにおいてもトランザクションが自動的に提供されます。通常の業務であれば、1つの HTTP リクエスト処理・非同期ジョブに1つのトランザクションがあればそれで十分ですが、ちょっと込み入ったことをやろうとすると、例えば

  • 監査用の操作ログ・クエリログをテーブルに出力したい
    →コミットされるタイミングは後でもいいんだけど、本流の処理で転けてロールバックかけた場合に、操作ログまでなかったことになるのは避けたい。
  • 非同期ジョブなどで、ジョブ管理テーブルを随時監視・更新したい
    →本流の処理のトランザクションはコミットしたくないけど、ジョブ管理テーブルへの変更は即座にコミットして反映させたい。
など、1つのトランザクションではちょっと実現が難しいこともあったりします。前者の例は、トランザクションを二つに分けたとして互いが重ならないように工夫できるでしょうが、後者はそう簡単にはいきません。互いのトランザクションが一部重なり、同時並列で存在しなければならない状況が発生し得ます。

ところで Play framework 的には、「一連の処理で複数のトランザクションを扱う」ことは「レールから外れた」行為である可能性が高く、このような行為を標準でサポートしているわけでもなく、当然ながら (私の知る限り) ドキュメント・リファレンスの類いに書かれているわけでもありません。


Play のエンティティと EntityManager

Play 標準では複数トランザクションを明示的にはサポートしていませんが、複数トランザクションを扱えないと困ることがあるのは事実なので、何とかしたいところです。何とかするには、まず Play がどのようにトランザクションを提供してくれているかを把握する必要があります。そこで、Play の実装、特にトランザクション周りをソースコードリーディングしていくことにします。

初めは、Play における永続化、特に トランザクションと関わりの深い EntityManager 周りの扱いを確認してみましょう。

EntityManager は、Play では play.db.jpa.JPA クラスフィールド entityManager で管理されています。JPA クラスのオブジェクト自体は、ThreadLocalクラスフィールド JPA.local で管理されています。ThreadLocal オブジェクトで管理されていることから、JPA オブジェクトそのもの、またそのフィールドにある EntityManager オブジェクトは、スレッド毎に用意されていることがわかります。

Play のエンティティは、この JPA.local クラスフィールドで管理されている、スレッド毎に用意された EntityManager を利用して永続化やクエリを実現しています。例えば、Model#save() メソッド から呼び出される play.db.jpa.JPABase クラス_save() メソッド を見てみましょう。JPABase.em() メソッドから始まり、呼び出し先を追っていくと JPA.em() → JPA.get().entityManager → JPA.local.get().entityManager と、ThreadLocal オブジェクトで管理されている JPA オブジェクトのフィールドにたどり着くことができます。Model#find() メソッド も同様で、その内部ではやはり JPA.local クラスフィールドで管理されている EntityManager を利用します。


トランザクションの開始・終了

トランザクションと関わりの深い EntityManager が、JPA.local にスレッド別になるよう管理されていることがわかりました。次に、トランザクションの開始と終了について確認してみましょう。

play.db.jpa.JPAPlugin クラス を見てみます。このクラスの startTx() クラスメソッド と、closeTx() クラスメソッド がそれぞれトランザクションの開始・終了処理を担当します。JPAPlugin.startTx() クラスメソッドの実装を見ると、その最後で JPA.createContext() クラスメソッド を呼び出しています。この JPA.createContext() クラスメソッドの実装を見て明らかなように、

  1. 既存の JPA オブジェクト、EntityManager オブジェクト (≒トランザクション) が存在する場合は、その EntityManager オブジェクトをクローズ(≠コミット)し、
  2. 新たな JPA オブジェクトを生成して、JPA.local クラスフィールドの ThreadLocal オブジェクトに設定する
という処理が、トランザクション開始時に行われます。つまり「新しいトランザクションを開始しようとすると、既存のトランザクションは破棄された上で新しいトランザクションが始まる」ということになります。

なお、JPAPlugin.startTx()/closeTx() 両クラスメソッドは、それぞれ HTTP リクエストに対応するコントローラ・アクションメソッドの呼び出し前後、非同期ジョブの実行前後に呼び出され、標準のトランザクションを構成します。


複数トランザクションを並列して扱う方法

上記のように、JPAPlugin.startTx() クラスメソッドを呼び出すことで新しいトランザクションの開始はできますが、古い・既存のトランザクションは破棄されて利用できなくなってしまいます。このままでは、複数のトランザクションを並列して扱うことはできません。それではどのようにして複数トランザクションを実現すればよいのでしょうか?

実は結構簡単で、新しいトランザクションを開始する前に、既存のトランザクション(JPA オブジェクト)を JPA.local より待避し、空っぽの状態にしてしまえばいいのです。

コードで表すと、以下のようになります

もとのトランザクションに復帰させたいときは、待避しておいた JPA オブジェクトを JPA.local に設定する(ThreadLocal#set())だけです。

実装がやや煩雑になってしまうのが難点ですが、待避した JPA オブジェクトを Stack で管理したり、または Map で管理するなどして、複数トランザクション管理機能をクラス化してしまえば多少は扱いやすくなるのではないでしょうか。


注意点

以上が Play で複数並列にトランザクションを扱う方法になりますが、1つ、注意しなければならないことがあります。

先に述べたとおり、JPAPlugin.startTx()/closeTx() クラスメソッドは HTTP リクエスト処理のアクションの前後に (暗黙的に) 呼び出されます。特に JPAPlugin.closeTx() については、アクションのメソッド内で例外が発生したとしても必ず呼ばれる実装になっています(例外が発生したら closeTx(true) でロールバック、例外発生がなければ closeTx(false) でコミットするように呼び出されます)。

しかし、この暗黙的な JPAPlugin.closeTx() 呼び出しでクローズされるのは、JPA.local に設定されている JPA オブジェクトのトランザクションだけであり、たとえば上記コードの 8 行目で待避した JPA オブジェクトについては、25 行目で再び JPA.local に戻されるまでの間に例外が発生すると、クローズされずに放置プレイ状態になってしまいます。

そのため、JPA.local から JPA オブジェクトを待避させる場合は例外発生に注意し、たとえ例外が発生したとしても、待避された JPA オブジェクトのトランザクションを確実に・明示的にクローズする実装が必要になるでしょう。