Wikipediaによると、 データベースマネージメントシステムに置けるアイソレーションレベルの定義は、 1つの命令による変化が、並行する他の命令に、いつどのようにして見えるようになるか、ということです。 この記事の目的は、Google App Engineデータストアでのクエリとトランザクションアイソレーションを説明することです。 この記事を読むと、トランザクション内外の、並行するデータストアへの読み書きがどのように振舞うか、 よりよく理解できるでしょう。
4つのトランザクションアイソレーションレベルを強いものから順に並べると、シリアライザブル、リピータぶるリード、 リードコミッティッド、リードアンコミッティッドとなります。データストアのトランザクションはシリアライザブルレベルを 満たします。それぞれのトランザクションは、他のトランザクションから完全に独立しています。 与えられたエンティティグループに対するトランザクションは1つ1つ直列に実行されます。
さらに、トランザクションでのクエリとgetは、 トランザクション中は、1つの一貫したデータストアのスナップショットに対して行われます。 詳しい情報は、Wikipediaのsnapshot isolationの記事や、トランザクションのドキュメントの独立性と整合性の部分を参照ください。
トランザクション外部でのデータストア命令は、リードコミッティッドレベルに近いです。 クエリやgetによって取り出されたデータストアのエンティティは、コミットされたデータからのみ構成されます。 取り出されたエンティティは、部分的にコミットされたデータということは決してありません (一部はコミット前、一部はコミット後、という風な)。 クエリとトランザクションの相互関係は細かいですが、 それを理解するためにはコミットプロセスをさらに詳しく見ていく必要があります。
コミットは2つのマイルストーンから構成されます。エンティティの変更が適用されるポイントと、 それらのエンティティのインデックスへの変更が適用されるポイントです。 最初のポイントをマイルストーンAとし、2つ目をマイルストーンBとします。 マイルストーンAに到達するまでに、すべてのエンティティへの変更が適用されます。 マイルストーンBに到達するまでに、エンティティのインデックスへの変更が適用されます。
更新されたエンティティを、マイルストーンAの後に、キーを用いてルックアップするリクエストに対しては、 最新のバージョンのエンティティが見えることが保障されます。 しかし、あるエンティティが更新前の条件を満たしておらず、更新後の条件をを満たしているような 述語(SQLのwhere節のこと。GQL fans out there【?】)をもつクエリをリクエストが発行した場合、 commit()命令がマイルストーンBを過ぎた後にクエリが実行された場合のみ、そのエンティティは結果に含まれることになります。 【クエリがマイルストーンA〜Bの間に実行されると、そのエンティティが結果に含まれないということ。】 言い換えると、僅かな期間中、クエリの述語を満たすプロパティのエンティティが結果に含まれない可能性もあり、 クエリの述語を満たさないプロパティのエンティティが結果に含まれる可能性もあります (キーよるルックアップの結果と照らし合わせた場合です)。 クエリは、どのエンティティを返すかということを決定する際に、マイルストーンAとマイルストーンBに起こった変更を考慮できないため、 クエリがあるエンティティを返すことを決めると、それは必ずマイルストーンA時点におけるエンティティになります。
更新とクエリの同時実行による作用の一般的な説明をしてきましたが、 もしあなたが私に似ているならば、具体例を見ることで、これらの概念を頭に入れることが簡単だと分かるでしょう。 少しやってみましょう。いくつかの簡単な例を試し、より面白い締めくくりにしましょう。
Personエンティティを保存するアプリケーションを作成しましょう。Persionは以下の属性を持ちます。
アプリケーションは次の命令を実行出来ます。
2つのPersonエンティティをデータストアに作成します。
ほぼ同時にアプリケーションが2つのリクエストを受けることを想定してください。 最初のリクエストは、Adamの身長を68インチから74インチに更新します。 成長スパート!2つ目のリクエストはgetTallPeople()リクエストを呼びます。 getTallPeople()は何を返すでしょう?
この答えは、1つめのリクエストによって誘発された2つのcommit()マイルストーンと、 2つめのリクエストにより実行されたクエリの関係に依存します。 以下のようだと仮定してください。
このシナリオでは、getTallPeople()はBobだけを返します。 何故でしょうか?理由は、Adamの更新がコミットされていないため、リクエスト2のクエリに変更が見えないからです。
次に、以下のような場合です。
このシナリオでは、リクエスト1がマイルストーンBに到達する前にクエリが実行されるので、 Personのインデックスの更新がまだ適用されていません。 その結果、getTallPeople()はBobだけを返します。 これは、「結果の集合が、クエリの述語を満たすエンティティを含まない」ということの例です。
この例では、リクエスト1に違うことをさせます。Adamの身長を68インチから74インチに増やすかわりに、 Bobの身長を73インチから65インチに減らします。もう一度、getTallPeople()がどうなるか見てみます。
このシナリオでは、getTallPeople()はBobを返します。何故でしょうか? 理由は、Bobの身長を減らすための更新がコミットされていないため、 変更がリクエスト2のクエリには見えないからです。
次に、以下のような場合です。
このシナリオでは、getTallPeopleは誰も返しません。何故でしょうか? 理由は、リクエスト2がクエリを実行するときにはすでに、 Bobの身長を減らすための更新がすでにコミットされているからです。
次に、以下のような場合です。
このシナリオでは、クエリはマイルストーンBの前に実行されており、Personのインデックスの更新は まだ適用されていません。 その結果、getTallPeople()はBobを返します。 しかし、返されるPersonエンティティの身長プロパティは65に更新されています。 これは、「結果の集合が、クエリの述語を満たさないエンティティを含む」ということの例です。
上の例で分かるとおり、Google App Engineデータストアのトランザクションアイソレーションレベルはリードコミッティッドに 非常に近いです。もちろん重要な違いはあります。しかし、あなた方は、すでにこれらの違いとその理由を理解しているので、 自分のアプリケーションで理にかなったデータストアデザインの決定を行える立場にあると思います。