2009年12月14日月曜日

SDK 1.2.8 Release Notesで語られなかったこと

先日App Engine SDK 1.2.8 for Javaがリリースされました公式のリリースノートに記載されていない非常に重要な機能追加がありますので、ここで紹介します。基本的にすべてデータストアのお話です。しかもLow-Level API。

ご指摘いただきまして、ミスリードしそうな個所について修正しました。今後もわかったことがあれば追記していく予定です。

プロダクション環境でカーソルが予想外の動きをしました。このため、開発環境と両方で動くコードに更新しました

カーソルの追加

これまでデータストアでページング処理を行う場合には、ページング用のプロパティを用意したり、ページング用のインデックスを用意したりと、様々な力技が提案されてきました。 しかし、今回のアップデートでCursorという「クエリの現在位置を覚えておくオブジェクト」がひっそりと追加されました。これを利用すると簡単にページング処理が行えますので、簡単に紹介します。

カーソルの取得

現在位置のカーソルを取得するには、PreparedQueryasQueryResult*()というメソッドを利用します。 このメソッドはasIterator(), asIterable(), asList()などと似た動作を行いますが、返されるオブジェクトにはいずれもgetCursor()というメソッドが追加されています。

こんな感じで使います。

Query query = new Query("Hoge");
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// asIterator() -> asQueryResultIterator() にして、
// 取得したいエンティティ数 + 1のリミットを指定
QueryResultIterator<Entity> iter =
    ds.prepare(query).asQueryResultIterator(
        FetchOptions.Builder.withLimit(10 + 1));

// 10件分だけ取得
for (int i = 0; i < 10 && iter.hasNext(); i++) {
    Entity entity = iter.next();
    // ...
}

// ここまでの結果をカーソルにする
Cursor cursor = iter.getCursor();
if (cursor == null) {
    // プロダクション環境では、末尾に達しているとnullになる模様
}

// カーソルはBase64エンコードされた文字列にもできる
String page = cursor.toWebSafeString();
// ...

ポイントは、リミットをあらかじめ指定しておくことです。 リミットに達した後に QueryResult*.getCursor()することで、リミットで指定した位置のカーソルを記憶しておいてくれます。 (2009/12/14時点のプロダクション環境では、Limitの末尾に達するとgetCursor()nullを返すようです)。上記のように、「取得したい件数 + 1」をリミットに指定して、イテレータで指定の回数だけフェッチするのがよさそうです。また、Iterator.hasNext()を実行してもカーソルは変化せず、Iterator.next()を実行するとひとつ分だけカーソル位置が変化するようです。イテレータ以外でどのように動くかはまだ追いかけきれていません。

カーソルの復元

先ほど作成したカーソルをもとに、そのカーソルの続きを取り出すことができます。

// Cursor.toWebSafeString()の文字列をカーソルに復元
Cursor cursor = Cursor.fromWebSafeString(page);

// カーソルをもとに、そこから20 + 1件分を取得するオプションを作成
FetchOptions restored = FetchOptions.Builder
    .withLimit(20 + 1)
    .cursor(cursor)
    ;

// 先ほどと同じクエリを再構築して、復元したカーソルを利用
Query query = new Query("Hoge");
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
QueryResultIterator<Entity> iter =
        ds.prepare(query).asQueryResultIterator(restored);

// 20件分だけ消費
for (int i = 0; i < 20 && iter.hasNext(); i++) {
    Entity entity = iter.next();
    // ...
}

// ここまでの結果もまたカーソルにできる
Cursor next = iter.getCursor();
if (next == null) {
    // ...
}

このように、「指定の件数だけ処理→カーソルを取得→カーソルを復元→指定の件数だけ処理→…」と繰り返すことで、ページング処理が簡単に実装できます。また、カーソルは Cursor.toWebSafeString(), Cursor.fromWebSafeString()を利用することで、クライアント側に安全に返すこともできます。

ただし、このカーソルは次の項で紹介するIN演算子、!=演算子と同時に使えないようです(SDK 1.2.8で確認)。その場合、getCursor()nullを返すようなので、カーソルが取れるか不安なときには戻り値をチェックしておくのがよさそうです。

IN演算子、!=演算子の追加

Python版同様に、Java版にもIN演算子、!= (NOT EQUAL)演算子が追加されました。FilterOperatorから確認できます。

FilterOperator.IN

FilterOperator.INは、「プロパティの値が指定した値のいずれかと一致する」というフィルタです。次のような感じで使います。

Query query = new Query("Hoge")
    .addFilter("value", FilterOperator.IN, Arrays.asList(10, 20, 30));

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Iterator<Entity> iter = ds.prepare(query).asIterator();
while (iter.hasNext()) {
    Entity entity = iter.next();
    // ...
}

上記のプログラムでは、次のようなエンティティの一覧を検出できます:

  • "Hoge" というカインドで
  • かつ "value" プロパティが 10 または 20 または 30

また、IN演算子はEquality Filterとして解釈されます。そのため、複数のプロパティに対して指定することも可能です。これに関して注意点もありますが、後の項でまとめて紹介します。

FilterOperator.NOT_EQUAL

FilterOperator.NOT_EQUALは、「プロパティの値が指定した値と一致しない」というフィルタです。次のような感じで使います。

// 引数にはIterable<(プロパティの型)>を渡す
Query query = new Query("Hoge")
    .addFilter("value", FilterOperator.NOT_EQUAL, 10);

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Iterator<Entity> iter = ds.prepare(query).asIterator();
while (iter.hasNext()) {
    Entity entity = iter.next();
    // ...
}

演算子の解釈と注意点

IN演算子と!=演算子は、いずれもデータストアが直接サポートしていません。そのため、アプリケーションサーバ上でいくらかの計算が行われます。実際にどんな計算が行われているのかを簡単に紹介し、その動作から導き出されるいくつかの制約についても簡単に言及します。

IN演算子を利用したクエリについて考えてみます:

... AND a IN (1, 2, 3)

これは、OR演算子を利用して次のように書き換えることができます:

... AND ( a == 1 OR b == 2 OR c == 3 )

これを変形すると、次のようになります

... AND a == 1
OR ... AND a == 2
OR ... AND a == 3

App Engineのデータストアでは、実際にはORという演算子は利用できません。このため、上記のクエリを1行ずつ通常のクエリとして実行し、結果をメモリ状で結合するという操作を Low-Level API で行います。そのため、他のクエリよりもパフォーマンスが悪くなりがちです。

さらに、IN演算子が2個以上含まれているクエリはもっと大変です:

... AND a IN (1, 2, 3) AND b IN (4, 5, 6)

まず、b IN ...の部分を展開します:

... AND a IN (1, 2, 3) AND b == 4
OR ... AND a IN (1, 2, 3) AND b == 5
OR ... AND a IN (1, 2, 3) AND b == 6

さらに、a IN ...の部分を展開します:

... AND a == 1 AND b == 4
OR ... AND a == 1 AND b == 5
OR ... AND a == 1 AND b == 6
OR ... AND a == 2 AND b == 4
OR ... AND a == 2 AND b == 5
OR ... AND a == 2 AND b == 6
OR ... AND a == 3 AND b == 4
OR ... AND a == 3 AND b == 5
OR ... AND a == 3 AND b == 6

といった具合に、aに指定された値の個数×bに指定された値の個数の数だけクエリが展開されてしまいます。あまり多くなりすぎると負荷も高くなるため、App Engineでは展開されたクエリの数が30を超えた場合にはエラーを返すようになっているようです。

次に、!= (NOT EQUAL)演算子はもう少し単純です。

... AND a != 10

上記は、IN演算子の時と同様に、ORを利用して表現されます。

... AND (a < 10 OR a > 10)

同様に二段論理に変換してみましょう:

... AND a < 10
OR ... AND a > 10

上記のように2個のクエリに分割されます。ここで重要なポイントは <, >などの==でない演算子を利用しているという点です。 このため、!=演算子を利用する際には、他のプロパティに対して<, >などの演算子を適用できなくなります。 また、<, >と同様に未定義のプロパティ(None)を検索できません。

もうひとつの制約として、!=同じプロパティに対して同一クエリ内で複数回適用できません。そのような指定をするとIllegalArgumentExceptionがスローされることを確認しました。

0 件のコメント:

コメントを投稿