appengineはRead Committed相当だがcommit()には2つのマイルストーンがあることを忘れてはいけない

ここに詳しく書いてあることを本エントリは書いているだけですが、重要な内容なので書いておこうと思います。
appengineのトランザクション分離レベルはRead Committedとほぼ同等です。主要なRDBMSの一般的なトランザクション分離レベルということです。しかし、ちょっとした落とし穴があります。


Read Committedとは

SELECT文は問い合わせが開始される直前までにコミットされたデータのみで、コミットされていないデータは参照しない。
コミット前のデータを読んだりしないということですね。(ダーティーリードはしない)

appengineのcommit()には2つのマイルストーンがある

commit()を実行すると下記の2つが順に実行されます。

  1. マイルストーン A (エンティティに対する変更)
  2. マイルストーン B (インデックスに対する変更)

データを作ってからインデックスを作っているんですね。これ自体は一般的なRDBと一緒です。
しかし、appengineでは一般的なRDBとcommitが完了したとされるタイミングが異なります。
一般的なRDBではインデックスが更新されてからcommitの完了とみなされ、その完了時に他のセッションがデータを読み込みできるようになります。appengineではマイルストーン A (エンティティに対する変更)の時点で他のセッションがデータを読み込みできるようになります。


これが何を意味するか?例を用いて下記の説明があります。
Person というエンティティを格納するアプリケーションがあるとします。Person には、次の属性があります。

  • Name
  • Height

このデータストアには、2 つの Person エンティティが存在します。

  • Adam、身長 173 cm
  • Bob、身長 185 cm

getTallPeople()は身長183cmを超えるすべての人を返します。

リクエスト 1のput()にてBob の身長を185cmから165cmに縮めます。ここで、getTallPeople() はどうなるでしょうか。
  1. リクエスト 1、put()
  2. リクエスト 1、put() --> commit()
  3. リクエスト 1、put() --> commit() --> マイルストーン A
  4. リクエスト 2、getTallPeople()
  5. リクエスト 1、put() --> commit() --> マイルストーン B

マイルストーンBの前にクエリが実行されています。したがって、Person のインデックスに対する更新はまだ適用されていません。
結果として、getTallPeople() は Bob を返しますが、返される Person エンティティの Height プロパティは更新後の値、つまり 165 となります。
この例は、エンティティのプロパティがクエリの述語には合致しないにもかかわらず、そのエンティティが結果セットに含まれる場合の例です。

インデックスが更新されていなくて、データが更新されている状態なので検索結果が取れちゃうんですね。でも、値は更新されているので165になってしまうということです。上記の例でappengineの場合はインデックスとデータの更新が別々にコミットされていることがわかります。appengineはRead Committedとほぼ同等という言葉の裏にはこんなことがあったんですね。何か問題が発生した際にこういう動きだと知っているのと知らないのでは全然違うので理解しておきたいところです。
今回の例でもわかるようにappengineは値による検索は行われません。Keyやインデックスを意識して実装することが求められます。同様に注意しないといけない点がいくつかありますが、これはまたの機会に書きたいと思います。