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について補足する続編も書きました。

2009年8月29日土曜日

#appengine java でローカル環境のデータファイルをほげってみる

目的はKind名の一覧を取得する事。Kind名さえわかれば、後は普通にDatastoreServiceFactory#getDatastoreService()を使って、Entityを読み込む事ができて、Entityが読み込めればそこから全propertyを取得できたりして、色々な事ができそう。ローカルからAppEngineにアップロードする時にも使えそうだし。Eclipse用にLocalDatastoreViewerPluginとかも作れそうだし。

……っていうエントリを書くつもりだったんだけど、SchemaやらEntityProtoやら色々要素が多すぎて、説明するのが大変に面倒になって来たので、ユーティリティクラスのコードだけ晒す事にしますw ゴメンナサイ、ゴメンナサイ。

AppEngineの起動と終了

Datastore周りの単体テストをする時とほぼおんなじ。

static final String DATASTORE_V3 = "datastore_v3";

static ApiProxy.Environment setUpDatastoreService(final String appId, final String versionId,
    String applicationFolder) {
  ApiProxy.Environment environment;
  ApiProxy.setEnvironmentForCurrentThread(environment = new ApiProxy.Environment() {
    public String getAppId() { return appId; }
    public String getVersionId() { return versionId; }
    public String getRequestNamespace() { return ""; }
    public String getAuthDomain() { return "hoge.com"; }
    public boolean isLoggedIn() { return true; }
    public String getEmail() { return "fuga@hoge.com"; }
    public boolean isAdmin() { return false; }
    public Map<String, Object> getAttributes() {
      Map<String, Object> map = new HashMap<String, Object>();
      return map;
    }
  });
  ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(applicationFolder)) {});
  return environment;
}

static void tearDownDatastoreService() {
  ((LocalDatastoreService) ((ApiProxyLocalImpl) ApiProxy.getDelegate())
      .getService(DATASTORE_V3)).stop();
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
}

Kind名の一覧を取得する

LocalDatastoreService#getSchema()ってのがあります。スキーマレスなのにgetSchame()とはなんのこっちゃってカンジですが、Datastoreのビューアが使っているのもこの情報です。

public static String[] getKinds(Environment environment) {
  LocalDatastoreService datastoreService = (LocalDatastoreService) ((ApiProxyLocalImpl) ApiProxy
      .getDelegate()).getService(DATASTORE_V3);
  Schema schema = datastoreService.getSchema(null, (new StringProto()).setValue(environment
      .getAppId()));
  List<EntityProto> entityProtoList = schema.kinds();
  List<String> kindList = new ArrayList<String>(entityProtoList.size());
  for (EntityProto entityProto : entityProtoList) {
    List<?> path = entityProto.getKey().getPath().elements();
    Element element = (Element) path.get(path.size() - 1);
    kindList.add(element.getType());
  }
  return kindList.toArray(new String[0]);
}

特定のproperty名の一覧を取得する

Kind名がわれば実行時に取得できるので、あんまり必要無いかも。ちなみに、JDOを使っているとPOJOで定義した覚えが無いpropertyが混ざってくると思います。楽観的排他制御とか、ListPropertyのExtentで定義した制御の為にJDOが独自のpropertyを付加したものです。また、残念な事にインデックス対象でないpropertyは一覧から取得できません。

public static String[] getProperties(Environment environment, String kind) {
  LocalDatastoreService datastoreService = (LocalDatastoreService) ((ApiProxyLocalImpl) ApiProxy
      .getDelegate()).getService(DATASTORE_V3);
  Schema schema = datastoreService.getSchema(null, (new StringProto()).setValue(environment
      .getAppId()));
  List<EntityProto> entityProtoList = schema.kinds();
  for (EntityProto entityProto : entityProtoList) {
    List<?> path = entityProto.getKey().getPath().elements();
    Element element = (Element) path.get(path.size() - 1);
    String type = element.getType();
    if (kind.equals(type)) {
      List<Property> properties = entityProto.propertys();
      String[] propertyNames = new String[properties.size()];
      for (int i = 0; i < propertyNames.length; i++) {
        propertyNames[i] = properties.get(i).getName();
      }
      return propertyNames;
    }
  }
  throw new RuntimeException("kind \"" + kind + "\" is not found.");
}

使い方

SDKのコンテナを使った環境で使用しているデータファイル(war/WEB-INF/appengine-generated/local_db.bin)を使いたい場合は、以前書いたエントリ「#appengine のテスト用初期データを作成する」を参考に、アプリケーションのversionIdを確認しておく必要があります。

@Test public void test() {
  Environment environment = setUpDatastoreService("shin1ogawa-app", "versionid.1", "war");
  try {
    String[] kinds = DatastoreUtil.getKinds(environment);
    for (String kind : kinds) {
      System.out.println(kind);
      String[] properties = DatastoreUtil.getProperties(environment, kind);
      for (String property : properties) {
        System.out.println("  " + property);
      }
    }
  } finally {
    tearDownDatastoreService();
  }
}

importの宣言

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.dev.LocalDatastoreService;
import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiBasePb.StringProto;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.api.DatastorePb.Schema;
import com.google.storage.onestore.v3.OnestoreEntity.EntityProto;
import com.google.storage.onestore.v3.OnestoreEntity.Property;
import com.google.storage.onestore.v3.OnestoreEntity.Path.Element;

2009年8月20日木曜日

#appengine の勉強会を開催させていただいた(資料)

先日株式会社ヌーラボの東京メンバの方々にGoogle App Engine for Javaの勉強会を開催させていただきました。GAE/JやAppsに詳しい、意外なゲストの方もおられてこちらも大変勉強になりました。皆様ありがとうございます。

その時の資料を株式会社トップゲート技術者サイトにて公開しておきました。まだ殆どGAE/Jに触っていないという技術者向けの入門編、といった内容になっています。でも、課金周りの説明とかがイマイチわかりにくいよなぁ?とか反省点もあるので、時々資料を修正するかもしれません。

宣伝

株式会社グルージェントの精鋭メンバによる、国内では初めてとなる(?)GAE/Jの書籍が出版されるようです。楽しみにしておきましょう。個人的には「どのような順番で何を説明する事で、読者の理解を促進するのか?」という点が一番気になってたりするw

2009年8月15日土曜日

#appengine でクエリの1000件超の結果件数を取得する方法

先のエントリで書いた#appengine でクエリの結果件数を取得する方法(1000件超とか)の続きです。

どーしても動的な条件に対する結果件数を…ていうなら、最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数えるという手もあるかもしれない。これもまた試してみよう。この方法なら1000件につき約110msなんで、7000件でも770msくらいで数えきれるはずだ。

気になって眠れないので引き続きこちらの方法も試してみた。

手法

AppEngineでのページング手法としてBest practices for writing scalable applicationsでも書かれているページング方法で、setKeysOnly()を使う場合、使わない場合のふたつのパターンを試してみた。どちらもasList()を使い、FetchOptionにはwithOffset(0).limit(1000)だけを指定している。setKeysOnly()を使った方は当然条件に使用するpropertyの値がfetchできないので、ページングに必要なEntityのみDatastoreService.get(key)でfetchしている。

結果、setKeysOnly()しない方は平均3642ms/7473件で、setKeysOnly()した方は平均1441ms/7473件となった。JDOを使ってアレコレして1000件以上取得する場合は7218ms/7473件だったから、随分と速くはなった。どーーーーしても1000件以上の件数をカウントしたい場合はlow-level APIで件数の為にページングする、といった手法が一番現実的ですね。それでも遅いけど!FetchOptionをもぅちっとカスタマイズすれば、まだ速くなるかなぁ?

ページング部分のコード

最近はgistが重いっぽいので久々にSyntaxHighlighterで。PersonというEntityのheightという、値が重複する可能性があるpropertyを条件に検索する、という想定。

static int countEntities(DatastoreService service, Double param) {
  int pageNo = 1;
  int totalCount = 0;
  PagingResultVO result = null;
  while (true) {
    Query query = new Query(Person.class.getSimpleName())
        .addSort("height", SortDirection.ASCENDING)
        .addSort("__key__", SortDirection.ASCENDING);
    if (pageNo == 1) {
      query.addFilter("height", FilterOperator.GREATER_THAN_OR_EQUAL, param);
      result = getListInPage(service, query);
      totalCount += result.count;
    } else {
      // まずは重複する可能性のあるpropertyをequality filterし、
      // 次ページの先頭となるpropertyをgreater thanでfilterしてクエリする。
      Key nextKey = result.nextEntity.getKey();
      Object nextParam = result.nextEntity.getProperty("height");
      query.addFilter("height", FilterOperator.EQUAL, nextParam)
          .addFilter("__key__", FilterOperator.GREATER_THAN_OR_EQUAL, nextKey);
      PagingResultVO dupResult = getListInPage(service, query);
      totalCount += dupResult.count;
      if (dupResult.hasNext) {
        // 1page分丸ごと取得できた。
        result = dupResult;
      } else {
        // 先の条件だと1page分に足りないので続きを取得する。
        // GT_OR_EQではなくGTを使う。
        query = new Query(Person.class.getSimpleName())//
            .addSort("height", SortDirection.ASCENDING)//
            .addSort("__key__", SortDirection.ASCENDING)//
            .addFilter("height", FilterOperator.GREATER_THAN, nextParam);
        result = getListInPage(service, query);
        totalCount += result.count;
      }
    }
    if (!result.hasNext) {
      break;
    } else {
      // 1000件取得している場合は、最後の1件が次pageにも登場するので1件分引いておく。
      totalCount -= 1;
    }
    pageNo += 1;
  }
  return totalCount;
}

static PagingResultVO getListInPage(DatastoreService service, Query query) {
  FetchOptions fetchOptions = FetchOptions.Builder.withOffset(0).limit(1000);
  List<Entity> list = service.prepare(query.setKeysOnly()).asList(fetchOptions);
  int size = list.size();
  if (size > 999) {
    return new PagingResultVO(service, size, list.get(999));
  } else {
    return new PagingResultVO(service, size, null);
  }
}

public static class PagingResultVO {
  public final int count;
  public final Entity nextEntity; // 次のページの最初のEntity
  public final boolean hasNext; // 次のページが存在するか。

  public PagingResultVO(DatastoreService service, int size, Entity nextEntity) {
    this.count = size;
    if (nextEntity != null) {
      // keyしか入っていないので、filterに必要なpropertyを取得する必要がある。
      try {
        this.nextEntity = service.get(nextEntity.getKey());
      } catch (EntityNotFoundException e) {
        LOGGER.warning("あるはずのKeyが見つからない!" + nextEntity.getKey());
        throw new RuntimeException(e);
      }
    } else {
      this.nextEntity = nextEntity; // null
    }
    this.hasNext = nextEntity != null;
  }
}

#appengine でクエリの結果件数を取得する方法(1000件超とか)

今日ひがさんと「クエリの結果件数の取得方法」についてTwitterで少しやりとりした。

  • Low-level APIのPreparedQueryだと1000件の上限があるが、JDOのQueryだと上限が無い
  • 確かに上限は無いけど、JDOでQuery#execute().size()はめっちゃ遅くて現実的ではない
  • JDOでもQuery#setResult("count(this)")は速い。でも、こいつは実はLow-level APIに丸投げなのでやっぱ1000件の上限に引っかかる。
  • JDOでもQuery#setResult("key")するとLow-level APIでいうsetKeysOnly()と同じ、キーのみクエリになる。でも1000件の上限に引っかからない!

大体こんなカンジで、最後の件はひがさんが発見してGoogle App Engine for Java GroupMLにPostもしていた。で、具体的にどれくらいのパフォーマンスなのかが気になったので試してみる。

以下の5つのパターンで試してみた。

  1. パターン1:普通に((List)Query#execute()).size()して件数を取得する。
    1000件超の件数も取得できるし、Entityの全フィールドを取得できる。
  2. パターン2:Query#setResult("count(this)")して、(Integer)Query#execute()で件数を取得する。
    これは当然のことながら件数が返るだけ。しかも1000件以上の時は取得できない(1000という件数が返される)。
  3. パターン3:Query#setResult("key")して、((List)Query#execute()).size()で件数を取得する。
    1000件超の件数も取得できるし、各Entityの主キーフィールドだけは取得できる。
  4. パターン4:Low-level APIを使う。DatastoreService#preparedQuery()#countEntities()で件数を取得する。
    件数のみ取得できる。
  5. パターン5:Low-level APIでQuery#setKeysOnly()してから、DatastoreService#preparedQuery()#countEntities()で件数を取得する。
    件数のみ取得できる。

当然ながら、どのパターンも「filterあり」で実行する。ソート対象は「filter対象のProperty、主キー」を使用する。

1000件超の件数を取得する場合

クエリの結果件数は7473件、全Entityの数は10万件くらい(cronでランダムなEntityを自動生成していたので、全部で何件か?が把握できていないw)。主キーはKeyを使っており、gae.encoded-pkなStringではない。22回ずつ試して最大最小を取り除いて平均してみた。試行が少ないけど、ぶれも少なかく安定した数値が出ていたのでまぁいいか、と。

  • パターン1:Query#execute()の実行時間の平均は62msで、取得したListのsize()メソッドの実行時間の平均が9140ms
  • パターン5:Query#execute()の実行時間の平均は19msで、取得したListのsize()メソッドの実行時間の平均が7218ms

素のクエリと比較すると、setResult("key")する方が20%以上速いよぅだ。

1000件以内の件数を取得する場合

クエリの結果件数は720件。極力1000件ギリギリにしたかったんだけど、いいクエリ条件を作れなかった、ゴメンナサイ。クエリ対象のKindは上記の1000件超と同じもの。

  • パターン1:((List)Query#execute()).size()の実行時間の平均は858.2ms
  • パターン2:(Integer)Query#execute()の実行時間の平均は99.2ms
  • パターン3:DatastoreService#preparedQuery()#countEntities()の実行時間の平均は97.8ms
  • パターン4:DatastoreService#preparedQuery()#countEntities()の実行時間の平均は104.4mssetKeysOnly()しない時よりも7msほど遅いけど、たぶん誤差ですな。
  • パターン5:((List)Query#execute()).size()の実行時間の平均は704.2ms

やっぱ件数の取得専用の機能を使う方が断トツでいい結果ですよ、と。

ちなみに、この1000件以内の場合に件数を取得したQueryと同じQueryでKeyを100件Fetchする場合(パターン2は無理だけど)、JDOを使ったパターン1とパターン3はJava的な速度で済むから平均1msで走査できる。パターン4とパターン5は、Keyを100件Fetchするのにそれぞれ平均148ms112msかかる。setKeysOnly()が効いています。パターン4でKey以外を100件走査するのに147msで、Keyの走査と同じ速度ですね。

まとめ

1000件以内とほぼ断定できているなら素直にJDOでQuery#setResult("count(this)")するか、Low-level APIでcountEntities()しよぅ、て事ですね。

1000件超の件数を知りたい場合は、今のところJDOでQuery#setResult("key")で件数を取得するのが一番マシな方法、…とはいえ7000ms/7000件だし、まぁ実用的ではない。1000件以上数えるような要件はとっとと捨ててLow-level APIを使いましょう、て事かな。実際1000件以上の件数を数える要件にそこまで価値がある事は少ないと思うし。1000件以上をカウントしたい場合でも、別途カウンタを用意して済むならそれがいいし(Shardingとかの手法の考慮が必要ですけどもね。あとTaskQueueもとっとと導入してくれないとツライ)。動的な条件による結果の件数、となるとそこまで価値は無いような気がするしなぁ。どーしても動的な条件に対する結果件数を…ていうなら、最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数えるという手もあるかもしれない。これもまた試してみよう。この方法なら1000件につき約110msなんで、7000件でも770msくらいで数えきれるはずだ。

追記

最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数える

無理です、setKeysOnly()したらページングできません。…と追記するつもりだったけど、最後の1件だけkey以外もFetchするって事ならsetKeysOnly()でもいけるか?…とか思ったりもする。