今回のイミュータブル エンティティの実装では、Cloud Datastore の最も重要な特徴は “エンティティ グループ” にあります。エンティティ グループはエンティティのグループであり、このグループでは以下の 2 つのことが保証されます。
- 1 つのエンティティ グループのみに対するクエリからは整合性のある結果が得られます。これは、書き込みの直後にクエリを行った場合、その結果は、その書き込みによる変更を反映することが保証されているということです。逆に、クエリの対象が 1 つのエンティティ グループに限定されていなければ、整合性のある結果は得られないかもしれません(データの陳腐化のため)。
- マルチエンティティ トランザクションは、1 つのエンティティ グループ内にのみ適用できます(この機能は最近、改良されました。Cloud Datastore は現在、エンティティ グループ間のトランザクションをサポートしています。ただし、トランザクションに含まれるエンティティ グループの数は 25 までに制限されています)。
この 2 つのことは、いずれも私たちの実装にとって重要です。Cloud Datastore 自体の詳細については
ドキュメントをご覧ください。
どのようにイミュータブル エンティティを実装したか
私たちは、1 つのエンティティに対して行った変更をすべて保存するとともに、エンティティの一般的な操作(取得、削除、更新、作成、クエリ)をサポートする方法を必要としていました。私たちが選択した全体的な戦略は、2 つのレベルの抽象化により、“データストア エンティティ” と “論理エンティティ” を使用することでした。個々の “データストア エンティティ” で “論理エンティティ” の個々のバージョンを表すようにしたのです。
私たちの API のユーザーは論理エンティティだけを操作し、それぞれの論理エンティティはデータストア エンティティを特定するキーを持ち、一般的な取得、作成、更新、削除、クエリの各操作をサポートします。これらの論理エンティティは、その論理エンティティのさまざまなバージョンを構成する実際のデータストア エンティティにバッキングされるわけです。最新の、あるいは最も Tip(先頭)のバージョンのデータストア エンティティが、論理エンティティの現在の値を表します。
まず、データ モデルがどのようなものかを見てみましょう。私たちは以下のようにエンティティを設計しました。
ユーザーがエンティティに変更を加えようとするたびに、必ず新しいデータストア エンティティが保存される仕組みになっています。最新のデータストア エンティティは
isTip の値が true に設定され、他のデータストア エンティティは false に設定されます。
後でこのフィールドを使ってクエリを実行し、最新のデータストア エンティティを特定することで、特定の論理エンティティを取得します。このクエリはデータストアで高速に実行されます。すべてのクエリがインデックスを持つ必要があるからです。また私たちは、各データストア エンティティが作成された日時のタイムスタンプも保存します。
versionId フィールドは、各データストア エンティティのグローバル一意識別子(GUID)です。この ID は、エンティティを保存するときに Cloud Datastore によって自動的に割り当てられます。
consistentId は論理エンティティを特定します。これは、私たちがこの API のユーザーに提供できる ID です。1 つの論理エンティティ内のデータストア エンティティは、すべて同じコンシステント ID を持ちます。
私たちは、論理エンティティのコンシステント ID を、チェーンの最初のデータストア エンティティの ID と同じにしました。コンシステント ID はある程度任意に決めることができ、どのような一意識別子を選んでもかまいません。しかし、低レベルの Cloud Datastore API がどのデータストア エンティティにもユニークな ID を割り当てるので、私たちはコンシステント ID として、最初のデータストア エンティティの ID を使うことにしたのです。
このデータモデルには、興味深い点がもう 1 つあります。それは
firstEntityInChain フィールドです。
上の図には示されていませんが、すべてのデータストア エンティティは親を持ちます(親を基準にエンティティ グループが決まります)。チェーン内の最初のデータストア エンティティが親として設定されます。
重要なのは、チェーン内のすべてのデータストア エンティティ(最初のものを含む)が同じ親を持ち、それゆえ同じエンティティ グループに属することです。そのおかげで、これらに対して整合性を持つクエリを実行できます。以上のものが必要な理由については後述します。
コードで定義された同じイミュータブル エンティティを以下に示します。私たちは素晴らしい
Objectify ライブラリと Cloud Datastore を使っており、以下ではこれらを活用したコード例を随時紹介します。
public class ImmutableDatastoreEntity {
@Id
Long versionId;
@Parent
Key<T> firstEntityInChain;
protected Long consistentId;
protected boolean isTip;
Key<User> savedByUser;
}
それでは、論理エンティティに対する一般的な操作をどのように行うのかを見ていきましょう。論理エンティティがデータストア エンティティでバッキングされることを念頭に置いてください。
作成を実行
論理エンティティを作成するときは、1 つの新しいデータストア エンティティを作成し、Cloud Datastore の ID 割り当てを利用して versionId フィールドを設定し、consistentId フィールドに同じ値を設定するだけです。
また、親キー(firstEntityInChain)として自身を指定します。後でこのエンティティをクエリできるように、isTip を true に設定する必要もあります。さらに、タイムスタンプとデータストア エンティティの作成者を設定し、このエンティティを Cloud Datastore に永続的に保存します。
ImmutableDatastoreEntity entity = new ImmutableDatastoreEntity();
entity.setVersionId(DAO.allocateId(this.getClass()));
entity.setConsistentId(entity.getVersionId());
entity.setFirstEntityInChain((Key<T>) Key.create(entity.getClass(), entity.versionId));
entity.setTip(true);
更新を実行
論理エンティティを新しいデータで更新するには、まずチェーン内の最新のデータストア エンティティをフェッチする必要があります(手順については、下の “取得” のセクションで説明します)。
次に、新しいデータストア エンティティを作成し、
consistentId と
firstEntityInChain を、フェッチしたデータストア エンティティのものに設定します。新しいデータストア エンティティの
isTip を true に設定し、フェッチしたデータストア エンティティのこのフィールドを false に設定します(既存エンティティの中で、このインスタンスだけを変更することに注意してください。つまり、100 % イミュータブルなわけではないということです)。
最後に、タイムスタンプとユーザー キーのフィールドを埋めます。これで、新しいデータストア エンティティを保存する準備が整いました。
ここで重要なポイントが 2 つあります。1 つは、新しいデータストア エンティティについては、保存時に Cloud Datastore が ID を自動的に割り当てるようにすればよいということです(ID を他の用途で使う必要はないからです)。
そしてもう 1 つのポイントは極めて重要です。それは、既存データストア エンティティのフェッチと、新旧のデータストア エンティティの保存は、同一のトランザクションで実行するということです。そうしないと、データの内部整合性がなくなるおそれがあります。
// start transaction
ImmutableDatastoreEntity oldVersion = getImmutableEntity(immutableId)
oldVersion.setTip(false);
ImmutableDatastoreEntity newVersion = oldVersion.clone();
// make the user edits needed
newVersion.setVersionId(null);
newVersion.setConsistentId(this.getConsistentId());
newVersion.setFirstEntityInChain(oldVersion.getFirstEntityInChain());
// .clone also performs the last two lines but just to be explicit this, just fyi
newVersion.setTip(true);
ofy().save(oldVersion, newVersion).now();
// end transaction
取得を実行
取得を実行するには、Cloud Datastore に対してクエリ操作を行わなければなりません。特定の
consistentId
を持ち、かつ
isTip が true に設定されているデータストア エンティティを検索する必要があるからです。
このエンティティが論理エンティティを表します。クエリを整合性のあるものにしたいので、“祖先クエリ” を実行する必要があります(つまり、特定のエンティティ グループに対してのみクエリを実行するように Cloud Datastore に指示する必要があります)。このクエリが動作するのは、特定の論理エンティティのすべてのデータストア エンティティが、同じエンティティ グループに属するようにした場合に限られます。
このクエリが返す結果は 1 つだけ、つまり論理エンティティを表すデータストア エンティティだけでなければなりません。
を false に設定するだけです。こうすることで、上に述べた “取得” の操作を行っても、結果が返されなくなります。その一方で、以下に述べるようなクエリは引き続き動作します。