2009年8月30日日曜日

#appengine JavaのLow-Level API(低レベルAPI)入門

追記

このエントリについては今後はWikiでメンテナンスしていきますので、最新の情報はWikiで確認して下さい。

追記

スティルハウス社の佐藤さんよりコメントを頂いたので一部の表現を修正しました。取り消し線+青字にしたりしてます。

追記

ひがさんよりコメントを頂いたので、最後のTransactionについてのソースコードのサンプルを一部修正しています。子Entityをput()する呼び出しの第一引数にTranscationを指定するように修正しました。

ここから

GAE/JのLow-level API(主にDatastore周り)については基本的にJavaDocしかなくて情報量が少ないと思ったので、それなりに使っていくための簡単な説明を書いてみる(@fumokmm氏の発言で思いつきました、Thx!)。

個人的な思いとしては、GAE/JのDatastoreについてJDOから入るのは間違いの元だJDOから入ると間違った理解をしやすい/ハマリやすいと思ってるんで、Low-level APIから入って、それからJDOを使っていくかLow-level APIで行くか、を選択するのが良いと思ってます。GAE/JのJDOを使う時は、まずはlow-level APIから入って、それから「JDOだとこんな事を便利にやってくれるんだ」とプラスαの部分を積んでいく方が良い。JDOから入るとどーしてもRDBのORMだという認識が頭から抜けずにはまる人が多いよぅに思うんですよね。スタンスとしてはJDOを使う事に否定的なわけではなく、学習の順序という話です。

Entity

low-level APIを使うと、JDOのようなPOJOにアノテーション、タイプセーフなpropertyにアクセサ…と言った物は無いです。全てのエンティティをEntityクラスとして扱う必要があります。

一番単純なコンストラクタ
Entity entity = new Entity(String kind)
Kind名が必須です。主Keyを指定しない事になるので、永続するタイミングで主Keyは自動生成されます。
JDOでいう@PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key;と同じ状態です。
主Keyを指定するコンストラクタ
Entity entity = new Entity(String kind, String keyName)
keyNameKeyを作成する時に使用する文字列のnameのアレです。Keyオブジェクトを渡す方がわかりやすいと思うんだけど、うっかりそれをやるとそのKeyを持つEntityを親としたEntityGroupを形成してしまいます(次に説明するAncectorKeyでのコンストラクタが適用されてしまう)。
親キーを指定するコンストラクタ
Entity entity = new Entity(String kind, Key ancestorKey)
low-level APIでEntityGroup(用は単なる親子関係ですね)を形成したい場合はこれを使います。第二引数で指定したKeyに対応するエンティティの子エンティティとなるエンティティが作成されます。EntityGroupといっても、結局「エンティティのKeyが親を持つか?持たないのか?」という事でしか無いのです。
JDOだと@Persistent @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true") private Key ancestor;という方法で子エンティティに親エンティティの主キーをバインドしたり、設定したりしてEntityGroupを形成するRelationshipの組み方がありますが、これが近いかもしれません。
主キーと親キーを指定するコンストラクタ
Entity entity = new Entity(String kind, String keyName, Key ancestorKey)
属性値の設定
void Entity#setProperty(String propertyName, Object propertyValue)
属性の名前を指定して、エンティティに値を格納します。ちなみに、BlobText等のインデックス対象外の属性の場合はsetUnindexedProperty()を使う必要があります。JDOのアクセサ経由の属性の設定と違い、属性名が文字列型なあたりがtype safeではありませんし、与える値もObject型なあたりがtype safeではありません。
属性値の取得
Object Entity#getProperty(String propertyName)
存在しない属姓名が指定されてもExceptionを投げずにnullを返してきます。こちらもJDOと違い、type safeではないのでJavaで使う場合はcastがウザイです。
指定された属性名の属性を持っているか?を確認する為にboolean hasProperty(String propertyName)っていうメソッドもあります。指定された属性名に対応する属性が存在しない場合にfalseを返します。指定された属姓名に対応する値がnullの場合でも、属性さえあればtrueを返しますので、属性が存在しない/存在するけどnull、の区別をつける事が出来ます。
また、JDOを使って書き込みを行っている場合は、エンティティのバージョン管理という意味の楽観的排他制御やListPropertyのインデックス等の制御をする為にJDOが独自に付加した属性が存在していたりします。
気をつけなければいけない点としてIntegerで書き込んた属性値も実際にはLongに変換されて書き込まれるという点です。当然、取得するとLong型で取得できてしまう。
特殊な属性値の取得
Kind名を取得するにはString getKind()、主キーを取得するにはKey getKey()、親キーを取得するにはKey getParent()を使います。
主キーを意味する属性名
主キーは特殊な扱いで、KEY_RESERVED_PROPERTYという定数で主キーを意味する属姓名が定義されています。値は"__key__"で、GAE/Pythonを使ってる人にはおなじみの値です。

Keyの部分以外は単純に言うとMapみたいなモンですからシンプルです。属性へのアクセスがtype safeじゃない(全てのエンティティをひとつのクラスで扱うんだから当然だけど)点が特徴的です。low-level APIではJDOとは違いスキーマレスという特徴を活用する事ができるので、その場合にはhasProperty()メソッドが大活躍するはず。

DatastoreService

Datastoreへアクセスする為のサービスクラスで、大概の処理はこのDatastoreServiceが担当するんですが案外シンプルです。そんなに機能が無いからですw。他のサービスクラスと同様に、サービスクラス用のファクトリからインスタンスを取得します。
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();

保存する

めちゃ簡単です。基本的には Key DatastoreService#put(Entity entity)または List<Key> DatastoreService#put(Iterable<Entity> entities)の2種類のメソッドです。

削除する

これも簡単。void DatastoreService#delete(Keys... keys)が基本形。他に、Iterable<Key>を引数にする事も出来ます。

Keyを指定して読み出す

Entity DatastoreService#get(Key key)
主キーを指定してEntityを取得します。
個人的にはあんまり使った記憶がありませんが、Map<Key, Entity> DatastoreService#get(Iterable<Key> keys)っていうメソッドもあります。
親キーを指定した取得に専用メソッドは無く、後述するQueryクラスを使用する事になります。

クエリする(1)

実際の動作はスキャンなので、クエリという単語だと誤解を生みそうですが、クラス名やドキュメントでもクエリって書かれてるのでここでもクエリと書いておきます。Queryクラスでクエリを組み立てますが、JDOやJPAを使っている人はjavax.jdo.Queryjavax.persistence.Queryとは別なので注意が必要かも。
このQueryオブジェクトを引数に使ってDatastoreService#prepare(Query query)を実行する事でPreparedQueryが返されます(次の項で説明)。

Kindを指定するコンストラクタ
Query query = new Query(String kind)
Kindと親キーを指定するコンストラクタ
Query query = new Query(String kind, Key ancestorKey)
フィルタ
Query addFilter(String propertyName, Query.FilterOperator operator, java.lang.Object value)
見たまんまです。Query.FilterOperatorにはEQUAL, GREATER_THAN[_OR_EQUAL], LESS_THAN[_OR_EQUAL]という定数があります。
ソート
Query addSort(String propertyName[, Query.SortDirection direction])
これも見たまんまです。Query.SortDirectionも想像の通りASCENDING, DESCENDINGが定数として定義されています。
キーのみクエリ
Query setKeysOnly()
重要。キーのみのスキャンを行う時はコレを使います。もちろん、取得できるのはKeyのみで、属性にはアクセスできないEntityが返ってきます。
JDOでいうsetResult("key")とほぼ同じです。
次のようなカンジで使います。
Query query = new Query("kind")
  .addFilter("height", FilterOperator.EQUAL, 100)
  .addSort("height", SortDirection.ASCENDING)
  .addSort("__key__", SortDirection.ASCENDING);

addFilter(), addSort()thisを返してくれるのでチェインして記述する事ができます。

コンストラクタに親キーのみ指定するものもあるんですが、それを使って何かが取得できた記憶が無いので使い方がよくわかってません。コレの使いどころがわかっている方、教えて下さると嬉しいです。

読み出す(2)

上の項目で説明したQueryを引数にしてDatastoreService#prepare(Query query)を実行する事でPreparedQueryが取得できるのですが、ここから「何を/どのように取得するか?」といったカンジになります。

重要な点として、1000件以上の結果は返せないという点です。とはいえ1000件以上取得できるJDOも、1000件以上を扱おうとすると遅くて実用的ではない事も多いので、何が何でも1リクエストに対して1000件以上を一気に!…って時は色々工夫が必要になります。工夫した結果、low-level APIにたどり着くのかもしれません。

FetchOptions

後述する、「結果セットをどのように取得するか?」で使用する取得メソッドで使用します。個人的にはasList()メソッドで必要だから常にoffset(0)を使っている程度で、あまり使いこなせていないので細かい説明ができません、ゴメンナサイ。でも大体名前の通りなんでしょう。

  • FetchOptions offset(int offset)
  • FetchOptions limit(int offset)
  • FetchOptions chunkSize(int offset)
  • FetchOptions prefetchSize(int offset)

結果セットをどのように取得するか?は以下のパターンがあります。一番良く使うのはasList()かな?

  • List<Entity> asList(FetchOptions fetchOptions)
  • Iterable<Entity> asIterable([FetchOptions fetchOptions])
  • Iterator<Entity> asIterator([FetchOptions fetchOptions])

結果セット以外に取得できる物として、以下があります。

Entity asSingleEntity()
名前の通り、一件だけ取得します。一件も無い時はnullが返されますが、フィルタの結果に2件以上存在した場合はPreparedQuery.TooManyResultsExceptionが投げられます。
int countEntities()
名前のとおり、結果セットの件数を返します。
JDOでいうsetResult("count(this)")と同じです。

ちなみに、sdk1.2.2での現象で不具合なのか仕様なのかよくわかりませんが、例えば"Kind"というKind内でkind1(ルート), kind2(kind1の子エンティティ)という、同じKind内のEntity同士でEntityGroupを構築した場合、Query("kind", kind1.getKey())でクエリするとkind1, kind2の2件が返されます。なんのこっちゃわかりません(kind2だけが返される事を期待するよね?)。

Transaction

これはJDOの時と似たような使い方が出来ます。コメントでひがさんより指摘を頂き、最初と比べて一部修正しています。put()メソッドの第一引数にtransactionを指定しています。

Entity parent = new Entity("kind");
parent.setProperty("property1", "hoge");

DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
Transaction transaction = datastoreService.beginTransaction();
try {
  Key parentKey = datastoreService.put(transaction, parent);
  List<Entity> children = new ArrayList<Entity>();
  Entity child1 = new Entity("child", parentKey);
  child1.setProperty("property1", "child1");
  children.add(child1);
  Entity child2 = new Entity("child", parentKey);
  child2.setProperty("property1", "child2");
  children.add(child2);
  datastoreService.put(transaction, children);
  transaction.commit();
} finally {
  if (transaction.isActive()) {
    transaction.rollback();
  }
}

大体こんなカンジで実用できると思います。low-level APIも結構かわいいやつなので使ってやって欲しいと思います。JDOと併用するのも良い方針だと思います。例えばJDOメインだけどマスタ系へのアクセスは常にlow-level APIとか、まぁ色々と考えられると思います。

追記

Relationshipについて補足する続編も書きました。

8 件のコメント:

Unknown さんのコメント...

子供のputも引数にトランザクションが必要だと思います。

sss さんのコメント...

コメントありがとうございます。

> putの引数にトランザクションは必要

ここは悩んでいる所なんです。put(Transaction tx, Entity entity)とput(Entity entity)の差が見当たらないんですよね。pythonでいうrunInTransactionな方法に慣れていると、transactionを渡したくなるんですが、渡さなければならないという理由が見当たらんので困ってます(どーもTransaction内で動作しているよぅに見える)。

「Transactionを渡しておくのが無難」というカンジで理解する方が良いのでしょうか。記事の方は修正を入れます。

Unknown さんのコメント...

put(Transaction txn, Entity entity) のjavadocに

Exhibits the same behavior as put(Entity), but executes within the provided transaction.

と書かれているので、トランザクション内で実行したい場合は、put(Transaction txn, Entity entity)を呼び出すほうがいいと思います。
呼び出さないとトランザクションに参加できないんじゃないかなぁ。

sss さんのコメント...

ありがとうございます。

ドキュメントに従って第一引数にTransactionを渡す方法で固定していこうと思います。

suyan さんのコメント...

Queryの「コンストラクタに親キーのみ指定するもの」ですが、Kindにかかわらず、共通して指定したキーをAncestorとして持つエンティティが取得できるのではないでしょうか?
aエンティティの子として作られたBカインドのbエンティティとCカインドのcエンティティ、…のような。
確かめていないのでただの予想ですが。

sss さんのコメント...

>> すぎゃーん

もちろんその予想で試してみたのですが、なーーんも帰って来てくれなかったのです。エラーにもならなかったです。
とはいえ試したのは1.2.0の頃だったかもしれんので、1.2.2でも試してみるとします。

suyan さんのコメント...

そうだったのですか…確かめもせずに適当なことを言ってすみません ><

sss さんのコメント...

いえ、気になった点はどんどんコメントしてもらった方がこちらも参考になって嬉しいです。

今回の件ならQuery(Key ancestorKey) が何も返してくれないサンプルコードを書いた方がわかりやすそーだ、という事に気づきました!

他にも随所でサンプルコードを挟んだ方がわかりよいんだろーなーと反省中。