Revision 5 as of 2012-01-03 08:26:42

Clear message
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.

インデックス

When using a traditional RDBMS, you become accustomed to issuing any ad-hoc SQL query you want and letting the query planner figure out how to obtain the result. It may take twelve hours to linear scan five tables in the database and sort the 8 gigabyte result set in RAM, but eventually you get your result! The appengine datastore does NOT work this way.

Appengine only allows you to run efficient queries. The exact meaning of this limitation is somewhat arbitrary and changes as Google rolls out more powerful versions of the query planner, but generally this means:

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

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

しかし実際には、必ずしもappengineがジョインを行わないとは限らない。

Actually, it's not quite true that appengine won't do joins. It will do one kind of join - a "zig-zag" merge join which lets you perform equality filters on multiple separate properties. But this is still an efficient query - it walks each of the property indexes in order without buffering chunks of data in RAM.

What you should be getting out of this is that if you want queries, you need indexes tailored to the queries you want to run.

To make this easier, the datastore has an innate ability to store each and every (single) property as "indexed" or "unindexed" (Entity.setProperty() vs Entity.setUnindexedProperty(). This allows you to easily issue a queries based on single properties. By default, Objectify defaults to setting all properties as indexed unless you flag the field (or class) with an @Unindexed annotation.

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.