Locked History Actions

GAE/Objectify/Concepts

コンセプト

以下は、ObjectifyとAppengine datastoreが混じった紹介だ。

さて君は、何らかのデータを永続化したいってわけだ。 たぶん、datastoreドキュメントを見て、こう思ったろう。 「なんてこったい。複雑すぎるよ!」 エンティティ、クエリ言語、フェッチグループ、デタッチ、トランザクションなんてものがあって、そしてこいつらは呪われたライフサイクルを持ってる。 でも、JDOの下には多くの単純さが隠されている。

最初にすべきことは、RDBに関する先入観をひとまず脇に置くということだ。 GAEのデータストアはRDBMSではない。 実際には、それはハッシュマップのように振る舞う。つまり、「値」をインデックス付けし、問い合わせ可能にするものだ。 データストアについて考えるときには、永続的なハッシュマップを想像して欲しい。

エンティティ

このドキュメントでは、エンティティについて多く述べていく。 エンティティとはデータストア内の「object's worth of data」だ(訳注:?)。 Objectifyを使えば、エンティティは君が定義した一つのPOJOクラスに結びつくようになる。 データストア内では、エンティティはEntityのハッシュマップのようなオブジェクトだ。 概念的には二つは同じものだ。

データストアとは、そもそも概念的にキーからエンティティへのハッシュマップであり、そしてエンティティは名前/値ペアのハッシュマップなのだから、概念モデルとしては、データストアとは、「ハッシュマップのハッシュマップ」ということになる。

操作

データストアに対する基本的な操作は4つしかなく、すべての永続的APIは結局のところ、そこにゆきつく。

  • データストアからエンティティ全体をget()する。一度に複数を操作することもできる。
  • データストアにエンティティ全体をput()する。一度に複数を操作することもできる。
  • データストアからエンティティをdelete()する。(ご想像通り)一度に複数行うことができる。
  • 任意のcriteriaにマッチするエンティティをquery()することができる。

キー

すべてのエンティティはLongのIDあるいは文字列名称を持つが、しかしそれだけではエンティティを特定することはできない。 データストア中ではエンティティはid(あるいは名称)とkindで特定される。kindとは、君がストアするオブジェクトのタイプに関連する。 つまり、Car #959をデータストアから取得しようと思えば、例えば、get_from_datastore("Car", 959)などという呼び出しを行う必要がある(まだまだ実際のコードじゃないよ)。

ところで、嘘ついた、ごめん。 実際には、エンティティを特定するために必要な第三の値があるんだ。それをペアレントという。 ペアレントは、特殊な「関係」を定義する。ある「子」はペアレントによって同じエンティティグループに置かれる。 エンティティグループについては、トランザクションに関する次のセクションで説明するけど、今知っておいて欲しいことは、 エンティティを特定するには、ペアレントも指定剃る必要があるということだ(ただし、これはnullであることが多い。つまり、ペアレントなしのルートエンティティということだ)。 ということはつまり、Car #959をデータストアから取得するためには、実際には get_from_datastore("Car", 959, null)とかget_from_datastore("Car", 959, theOwner)とかいう呼び出しを行う必要があるということ。

こういったパラメータを常に渡す代わりに、データストアはこれらの値を一つのオブジェクト(つまり、キーね)にラップしている。 これがキーについてのすべてだ。こいつは、エンティティを特定するための3つのパートのホルダであるということ。

ネイティブなデータストアキークラスは単純で型がない、ネイティブなエンティティクラスのようにね。 が、Objectifyでは型情報を保持するジェネリックなKeyを提供している。

Key<Car> rootKey = new Key<Car>(Car.class, 959);
Key<Car> keyWithParent = new Key<Car>(parent, Car.class, 959);

Keyを指定して、Objectifyインターフェースの基本的なメソッドを使うことができるのだが、 これらの操作はDatastoreServiceとほぼ同じものだ。 もしジェネリックスがわからなくとも、もう少し待って欲しい、あとで例を出すから。

<T> T get(Key<? extends T> key) throws EntityNotFoundException;

Objectifyでは、君のオブジェクトは、必須識別子(Long,longあるいはString)を持ち、オプションとしてペアレントの指定されたJavaクラスとして定義する必要がある。が、オブジェクトのルックアップにはKeyを使用する必要がある。 また、複数のリクエストをひとつにまとめた呼び出しとしてバッチ処理することができるが、 複数の異なるkindのオブジェクトをフェッチすることもできる。

Map<Key<Object>, Object> lotsOfThings = objectify.get(carKey, airplaneKey, chairKey, personKey, yourMamaKey);

また嘘をついた、ごめん。 いつでもキーを手で作らないということは無いんだ。 一つのタイプのオブジェクトについての典型的なケースについては便利なソートカットがある。 ただこれは、単にキーを生成して、get()を呼び出すだけに過ぎないことも忘れないでくれ。

Car c = objectify.get(Car.class, 959);
Map<Long, Car> cars = objectify.get(Car.class, 959, 911, 944, 924);

ところで、キーはリレーションの参照としても使用できる。 Remember that value that defines a parent entity? このペアレントのタイプはキーだ。

public Key(Key<?> parent, Class<? extends T> kind, long id)

システム内の他のエンティティへのリレーションを作成する場合、 エンティティリレーションのタイプはキーにする必要がある。

トランザクション

データストアは、JTAインターフェースの作成をファシリテートするため、たくさんの奇妙なコンセプトに満ちている。thread local transactions, implicit transaction management policies, and methods that behave differently whether you pass them a transaction or not. 全部忘れてくれ。覚える必要があるのはエンティティグループだ。

エンティティグループ

エンティティをput()すると、それは数千のマシンのgigantic farmのどこかの場所に保存されてしまう。アトミックなトランザクションを実行しようとすれば、(現在のところ)そのトランザクションに関わるすべてのデータが同じサーバ内にあることが必須だ。どこにデータがストアされるかを、君自身が制御できるようにするための方策として、データストアはエンティティグループという概念を提供している。

ペアレントがキーの一部であることを覚えているだろうか? エンティティがペアレントを持つなら、それは、それを親とする同じエンディティグループに属する。 エンティティがペアレントを持たないなら、それはエンティティグループのルートに属し、物理的にクラスタ内のいずれの位置にも格納される可能性がある。

トランザクション内では、一つのエンティティグループに属するデータにしかアクセスすることはできない。複数のエンティティグループのデータにアクセスしようとすれば例外が発生する。つまり、これはエンティティグループの選択は慎重にしなければならないことを意味する。 usually to correspond to the data associated with a single user. Yes, this severely limits the utility of transactions.

なぜすべてのデータに共通のペアレントを作り、すべてをひとつのエンティティグループにいれないのかって?もちろんできるけど、バッドアイデアだな。 Googleは、一つのエンティティグループでサービス可能な一秒あたりのリクエスト数を制限しているからだ。

ペアレントという言葉は誤解を招きやすいことを言っておくべきだろう。 データストアには「cascading delete」は無い。 ペアレントエンティティを削除しても、子を削除することにはならない。

  • For that matter, you can create child entites with a parent Key (or any other key as a member field) that points to a nonexistant entity! Parent is only important in that it defines entity groups; if you do not need transactions across several entities, you may wish to use a normal nonparent key relationship - even if the entities have a conceptual parent-child relationship.

トランザクションの実行

get(),、put()、delete()あるいはquery()を実行する場合には、トランザクション内でもよいし、外でもよい。

トランザクション内で実行するなら

  • オブジェクトのget/put/delete/query操作は、単一のエンティティグループのものにしか行うことができない。
  • クエリは先祖(ルートエンティティなど)を含める必要がある。
  • すべての操作は完全に成功するか完全に失敗するかのいずれかである。get()とクエリ操作は、トランザクション開始時に凍りついてしまったかのような状態でデータベースを見ることになる。トランザクション内で行ったput()やdelete()は反映されない。

もし、こちらがコミットする前に、他のプロセスがデータを変更してしまったら、こちらの操作はConcurrentModificationException 例外を伴って失敗する。

トランザクション外で実行するなら

それぞれのデータストア操作は別々に取り扱われる。

Any changes to the datastore made anywhere will have immediate effect - successive get() operations may return different values. If there is contention, operations will be automatically retried until the operation succeeds or the system gives up.

インデックス

伝統的なRDBMSでは、SQLクエリを発行する場合、クエリプランナに対してどのように結果を取得するかを示させることができるだろう。 例えば、5つのテーブルをリニアサーチするのに12時間かかり、8GBの結果セットをメモリ内でソートする必要があるかもしれないが、最終的には結果を取得することができるだろう。 しかし、appengineのデータストアはこのようには動作しない。

appengineは「効率的なクエリ」しか許さない。 この制限の性格な意味は、少々恣意的でGoogleがよりパワフルなクエリプランナをリリースすると変わるかもしれないが、しかし一般的にこれは以下を意味する。

  • テーブルスキャンも無く、ジョインも無く、メモリ内ソートも無い

データベースクエリプランナはただ一つの操作だけを好む。 インデックスを見つけ、その順番でたどる。 これは、クエリを行うには以下の条件が満たされなければならないことを意味する。 つまり、フィルタリングしたいフィールドあるいは複数のフィールドはあらかじめ適切にインデックス付けされていなければならない。 さらに、appengineはジョインを行わないため、クエリは単一のインデックスに詰め込むことのできる範囲に制限される。一つのプロパティでフィルタリングし、異なるものでソートすることはできないのだ。

しかし実際には、必ずしもappengineがジョインを行わないとは限らない。 これは複数の異なるプロパティについて等価フィルタを実行した場合のジグザグマージジョインという、ただ一つの種類のジョインは行うことができる。 ただし、これは依然として効率的なクエリだ。メモリ上にデータのバッファリングをせずに、各プロパティインデックスをたどることができるからだ。

何らかのクエリが必要であるなら、そのクエリを実行する目的のためのインデックスが必要になる。

これが簡単にできるよう、データストアにはすべての(単一の)プロパティについてindexedあるいはunindexed(Entity.setProperty() vs Entity.setUnindexedProperty())と指定することのできる生来の機能がある。 これによって、単一のプロパティについてのクエリを簡単に発行することができる。 デフォルトでは、Objectifyはすべてのプロパティをindexedとする。ただし、@Unindexedアノテーションをつけることによって、unindexedにすることもできる。

To run queries by filtering or sorting against multiple properties (that is, if it can't be satisfied by a zigzag merge on single-property indexes), you must create a multi-value index in your datastore-indexes.xml. There is a great deal written on this subject; we recommend How Entities and Indexes are Stored and Index Building.

Note that there are some tricks to creating indexes:

Single property indexes are created/updated when you save an entity. Let's say you have a Car with a color property. If you save a Car with color unindexed, that entity instance will not appear in queries by color. To index this entity instance, you must resave the entity. Multi-property indexes are built on-the-fly by appengine. You can add new indexes to your datastore-indexes.xml and appengine will slowly build a brand-new index - possibly taking hours or days depending on total system load (index-building is a low-priority task). In order for an entity to be included in a multi-property index, each of the relevant individual properties must have a single-property index. If your Car has a multi-property index on color and brand, an individual car will not appear in the multi-property index if it is saved with an unindexed color.

Now that you are familiar with the underlying concepts of the datastore, read the IntroductionToObjectify.