2009年9月23日水曜日

#appengine で親Keyのみを使ったQueryの動作について

一部修正しました

Kind Indexについて勘違いしており、誤用していました。ひがさんから指摘を受け、気づきました、ありがとうございます。

ここから本文

以前書いた#appengine JavaのLow-Level API(低レベルAPI)入門というエントリで、com.google.appengine.api.datastore.Queryクラスの「親Keyのみ指定するコンストラクタ」について以下のように記述した。

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

で、最近JUnitのテストケースをデプロイ環境で実行するServletを作っていて、そこで以前から作っているAPIの動作確認用のテストケースを偶然実行して気づいたのですが、親Keyを引数にするコンストラクタで作成したQueryは、想像通り(期待する通り)の動作をする、という事です。ローカルでは動作しませんが、デプロイした環境では動作します。

例えば、以下のような状態の時。

  • Kind1...kind1a, kind1b
  • Kind2...kind2a(kind1aの子),kind2b(kind1bの子),kind2c(kind1bの子)
  • kind3...kind3a(kind2a)の子,kind3b(kind2bの子)

上記の状態で、new Query(kind1a.getKey())すると、kind1a, kind2a, kind3a、が返されます。つまり、new Query("kind1", kind1a.getKey()), new Query("kind2", kind1a.getKey()), new Query("kind3", kind1a.getKey())をマージしたのと同じ結果です。つまり全てのKindを対象にしてEntityGroupがひとつ丸ごと取得できます。便利です。

Kindを指定しないならインデックスはどうやって定義するの?と思いますが、このクエリにはインデックスの定義が必要ありません。たぶんKindIndexテーブルのキーのprefix scanのみで動作しているんでしょうね。KindIndexテーブルのキーのprefix scanのみで動作するって事は…そぅ、sortもfilterもできません。ちなみに先の例で行くと、KindIndexテーブルのキーのprefix scanに使用されるIndexは次のようになっています。

  • kind1a
  • kind1a/kind2a
  • kind1a/kind2a/kind3a
  • kind1b
  • kind1b/kind2b
  • kind1b/kind2b/kind3b
  • kind1b/kind2c

というワケで、KindIndexテーブルのキーのprefix scanだけを使ってEntityGroupが簡単にごそっと取れる「べき」なのに、今まではローカルだけで試していて動作しない→「なんで?」となっていました。ローカルで動作しない場合も、デプロイ環境でも念のためチェックしとかなきゃダメですねぇ。

2009年9月19日土曜日

#appengine の単体テスト環境でTaskQueueを有効にする

有効に、といっても、defaultQueue()に関しては特に何もしなくても問題なくQueueFactory.getDefaultQueue().add()できます。queue.xmlで独自に追加したQueueを使用する方法について。簡単な事だけど、一瞬困ったのでメモ書き。

単体テストの最初に行う準備処理のApiProxy.setDelegate(new ApiProxyLocalImpl(new File(folderName)) {}で指定するフォルダのWEB-INF直下にqueue.xmlをコピーするだけでした。よくよく考えたらソースを追いかけるまでもなかったですね…。うっかりソースを深追いしてしまってますたw

File dstQueueXmlFile = 
  new File(folderName + (folderName.endsWith(File.separator) ? "" : File.separator) 
  + "WEB-INF" + File.separator + "queue.xml");
FileUtils.copyFile(new File(srcQueueXmlPath), dstQueueXmlFile);

自分の場合は上記のようにしてやってます。folderNameってのがApiProxyLocalImplのコンストラクタに渡すテスト用のフォルダ名です。

2009年9月15日火曜日

#appengine JavaではJDOは捨てましょう!

Slim3のMLに次のようなタイトルの議題が上がっていた。

自分はSlim3は触らないからMLに投稿できないけど、この方向はすごく良い事だと思いました。

スレッド内で少し出ている「App Engine以外では動かなくなる 」は全く問題になら無いと思います。なぜなら、GAE/J用のシステムは設計レベルでGAEに特化してしまうので、結局GAE/J以外に移行するには設計からやりなおす事になり、そーなると実装レベルでJDOを適用して…という話にはならんと思うからです。

low-level APIのメリットその一

これはずっと前からの意見なんですが、まず学習コストが低いですし、BigTableを勘違いせずストレートに理解しやすいんじゃないかと思います。JDOから入ると、従来のORMに使い慣れた人が従来のORMとして使ってしまい、あれ?とはまり込む気がします。特に、JDOのBigTable向け実装の不具合(datanucleus)にはまったりしたら何が何やら?となって最悪です(JPAはもっとヒドイかも)。JDOに関しては細かい状態(persistent, detachedだけではなくpersitent-dirtyとかそんなレベル)がJDOの楽観的排他制御等の処理とからむとかなりややこしいと感じるんですが、この細かい状態の遷移に関してかつて不具合を満載していたりした歴史もあります。ちなみにこれらはJDOという仕様についての話ではなく、JDOの特定の実装の話です、念為。

個人的に最も気に喰わないのは、JDOの仕様よりもはるかに機能が少ないBigTableという実装に対してJDOというインターフェースがかぶさっている事ですね。あえて勘違いを引き起こす為の悪質な罠という気がしてなりません。

low-level APIのメリットその二

最近よく話題になっている、トランザクションの整合性を自前で保証…というような話でもLowLevelAPIは便利に使えます。

制御可能なバッチPUT

SDK1.2.5のリリースと共にJDOでもバッチPUTができるようになるかも?という話があったものの、結局は一部のメソッドだけが対応されただけで、通常のORM的にJDOを使っている人はこの対象から漏れてしまうという残念な結果でした。当然LowLevelAPIは自由自在に制御できます。JDOを使っている人は一回だけ保存操作をしただけのつもりでも、実際にはアプリケーションノードとデータストアサービスノードの間を何往復かしていたりします。

EntityGroupの形成の制限がない

これは私がJDOを理解しきっていないという事で勘違いもあるかもしれませんが、JDOでは同じKind内でのEntityGroupが形成できないんじゃないか?と思います。LowLevelAPIではこれが可能です。例えば、KindA内のA1を親としてA2,A3がその子供としてぶらさがる…といった構造。

SDK1.2.5で追加されたallocateIds()

名前の通り、自動採番時のKeyのIDを割り当ててもらうだけの機能です。トランザクションの外で使用できます。これを使うと、例えば以下のような状況で嬉しいかもしれません。

KindA, KindBがOneToManyで、(A1/B1,A1/B2,A1/B3),(A2/B4, A2/B5,A2/B6)のふたつのEntityGroupにまたがる更新を行いたい
  1. A1/B1,A1/B2,A1/B3をバックアップKind(Backup)に保存する為に、まずはA1用のKeyを確保(Backup(A1)とする)
  2. 次にそのKeyを親として、さらにB1,B2,B3用のKeyを確保(Backup(B1)...)
  3. これでA1,B1,B2,B3,B4をBackupKindに保存する為のKeyが全て揃ったので、バックアップ対象のEntityはシリアライズする等してそれぞれのBackup用Entityの属性に詰め込んでおきます。
  4. 一つ目のEntityGroupのバックアップ用のトランザクションを開始する。
  5. PUT(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))する。JDOでこれをやると二回のPUTになってしまいますね。
  6. 一つ目のEntityGroupのバックアップ用のトランザクションをcommitする。

もちろん、この途中で失敗したら、この先の操作の整合性を保証できなくなるため、処理を先に進めないようにエラーを発生させる事になりますね。ポイントは、トランザクション内で一回のPUT操作でバックアップが完了する事です。

  1. 一つ目のEntityGroup更新用のトランザクションを開始する
  2. (A1/B1,A1/B2,A1/B3)を更新する。low-levelAPIオンリーならここも一回のPUT操作で終了ですね。
  3. 正常に処理できれば、commit。

異常が発生した場合はrollbackして、Backup(A1)の削除を行います。low-levelAPIであれば、BackupKindに対してBakup(A1)というAncestorKeyクエリを行えば必要なデータ(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3)が一発で取得できますので、delete((Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))するだけですね。ここをTaskQueueで処理できればさらに良いかもしれませんね。

さらに次のEntityGroupでも同様に処理し、もしそっちが失敗した場合はGET(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))すれば一回のGET操作で全てバックアップから取得できますね。

こんなカンジに処理を進めて、全て正常終了すればBackup(A1), Backup(A2)をキーにBackupKindを掃除しても良いし、放置しても良いし、TaskQueueで掃除しても良いかもしれませんし、そのあたりはアプリケーションの方針次第。

こんな風に、自前で整合性を維持するための裏方の処理はLowLevelAPIで処理し、極力本来の処理の邪魔をしないようにするのが良いと思います。で、そーこーしているウチにLowLevelAPIだけ使っちゃってる、という事になるんじゃないかなぁ?という風にも思います。

2009年9月11日金曜日

geronimo-jpa_2.0_spec ?

日本時間9/10夜あたりから、今までずっと使って来た AppEngine + Maven のビルドが急に通らなくなった。

1) org.apache.geronimo.specs:geronimo-jpa_2.0_spec:jar:1.0-EA2-SNAPSHOT

  Try downloading the file manually from the project website.

  Then, install it using the command: 
      mvn install:install-file -DgroupId=org.apache.geronimo.specs -DartifactId=geronimo-jpa_2.0_spec -Dversion=1.0-EA2-SNAPSHOT -Dpackaging=jar -Dfile=/path/to/file

  Alternatively, if you host your own repository you can deploy the file there: 
      mvn deploy:deploy-file -DgroupId=org.apache.geronimo.specs -DartifactId=geronimo-jpa_2.0_spec -Dversion=1.0-EA2-SNAPSHOT -Dpackaging=jar -Dfile=/path/to/file -Durl=[url] -DrepositoryId=[id]

  Path to dependency: 
   1) org.datanucleus:maven-datanucleus-plugin:maven-plugin:1.1.3
   2) org.datanucleus:datanucleus-jpa:jar:2.0.0-m2
   3) org.apache.geronimo.specs:geronimo-jpa_2.0_spec:jar:1.0-EA2-SNAPSHOT

geronimo-jpa_2.0_specってのがどこのリポジトリにも見当たらない。

pluginの依存を切る

今までpluginの依存をexclusionする方法を知らなかったけど、妙な記述でできたのでメモっておく。maven-datanucleus-pluginで定義されているdatanucleus-jpaへの依存を再定義しているようで気持ち悪い。

<pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.datanucleus</groupId>
      <artifactId>maven-datanucleus-plugin</artifactId>
      <version>1.1.3</version>
      <dependencies>
        <dependency>
          <groupId>org.datanucleus</groupId>
          <artifactId>datanucleus-jpa</artifactId>
          <version>2.0.0-m2</version>
          <exclusions>
            <exclusion>
              <groupId>org.apache.geronimo.specs</groupId>
              <artifactId>geronimo-jpa_2.0_spec</artifactId>
            </exclusion>
          </exclusions>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</pluginManagement>

今回はこれで凌げたけど、そもそもdatanucleus-jpa:2.0.0-m2に依存してしまうという状況が謎。よくよく見てみると、maven-datanucleus-plugindatanucleus-jpa二大して[1.1, 1.1.99)というバージョンの範囲(1.1-1.1.98)で指定されている。つまり、2.0.0-m2は範囲外のはずなんだよなぁ。なんでこんな動きをするのかなぁ。maven-datanucleus-pluginじゃなくてdatanucleus-enhancerを使ってみた方が良いのかなぁ。

2009年9月1日火曜日

#appengine JavaのLow-Level API入門 Relationship編

前回のエントリが結構好評だったのと、前回のエントリだけだと、JDOから入った人は「Relationshipどーすんの?」となりそぅな気がしたので、調子に乗ってlow-level APIとJDOでのRelationshipの比較等について書きます。

  • JDOの方はLLParentクラスがOneToOneでLLChildAを、OneToManyでLLChildBを保持するような構成です。
  • 一方low-level APIではJDOと同じ事をキーのみでEntityGroupを構成します。LLParentkindは子エンティティのためのpropertyを一切保持しないと言う事です。JDOの方でもキーのみでEntityGroupを構成する事ができますが、ListPropertyで保持する方が一般的というかORMっぽいかなぁと思いまして。

一見、データの保持の仕方が全く違うよね?と見えますが、実はどちらの方法で保存しても結局はDatastoreには全く同じ状態で格納されます。それを確認するために以下の組み合わせをテストケースで確認します。「JDOによる保存」「low-level APIによる保存」それぞれの結果に対して「JDOでクエリ」「low-level API」をそれぞれ試す、という4パターンを実際のコードで説明します。ばかみたいにコードをそのまま貼付けているので縦に長いです、ゴメンナサイ。

JDO用のエンティティクラス

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
@Version(strategy = VersionStrategy.VERSION_NUMBER, column = "version")
public class LLParent implements Serializable {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Key key;

  @Persistent
  private String property1;

  @Persistent(defaultFetchGroup = "true")
  private Text property2;

  @Persistent(defaultFetchGroup = "true")
  private LLChildA childA;

  @Persistent(defaultFetchGroup = "true")
  @Order(extensions = @Extension(vendorName="datanucleus", key="list-ordering", value="property1"))
  private List<LLChildB> childBList;

  ...以下アクセサメソッド
}

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
@Version(strategy = VersionStrategy.VERSION_NUMBER, column = "version")
public class LLChildA implements Serializable {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Key key;

  @Persistent  private String property1;

  ...以下アクセサメソッド
}

LLChildBクラスはLLChildAクラスとクラス名以外は全く同じです。

JDOによる保存

private Key saveByJDO() {
  LLParent parent = new LLParent();
  parent.setProperty1("parent:property1value");
  parent.setProperty2(new Text("parent:property2value"));

  LLChildA childA = new LLChildA();
  childA.setProperty1("childA:property1value");
  parent.setChildA(childA);

  List<LLChildB> childBList = new ArrayList<LLChildB>();
  LLChildB childB1 = new LLChildB();
  childB1.setProperty1("childB1:property1value");
  childBList.add(childB1);
  LLChildB childB2 = new LLChildB();
  childB2.setProperty1("childB2:property1value");
  childBList.add(childB2);
  parent.setChildBList(childBList);

  PersistenceManager pm = factory.getPersistenceManager();
  Transaction transaction = pm.currentTransaction();
  transaction.begin();
  parent = pm.makePersistent(parent);
  transaction.commit();
  pm.close();
  return parent.getKey();
}

特に変わった事はしてません。

low-level APIによる保存

private Key saveByLowLevelApi() {
  Entity parent = new Entity(LLParent.class.getSimpleName());
  parent.setProperty("property1", "parent:property1value");
  parent.setProperty("property2", new Text("parent:property2value"));

  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  com.google.appengine.api.datastore.Transaction transaction = service.beginTransaction();

  Key parentKey = service.put(transaction, parent);

  List<Entity> entities = new ArrayList<Entity>();
  Entity childA = new Entity(LLChildA.class.getSimpleName(), parentKey);
  childA.setProperty("property1", "childA:property1value");
  entities.add(childA);

  Entity childB1 = new Entity(LLChildB.class.getSimpleName(), parentKey);
  childB1.setProperty("property1", "childB1:property1value");
  entities.add(childB1);
  Entity childB2 = new Entity(LLChildB.class.getSimpleName(), parentKey);
  childB2.setProperty("property1", "childB2:property1value");
  entities.add(childB2);

  service.put(transaction, entities);
  transaction.commit();

  return parentKey;
}

こちらも特に変わった事はしてません。前回のエントリの最後にあったものとほぼ同じ。JDOによる保存とは違い、LLParentkindは子エンティティを保持するpropertyを持ちません。

JDOによる取得とassert

private void assertByJDO(Key parentKey) {
  PersistenceManager pm = factory.getPersistenceManager();
  LLParent entity = pm.getObjectById(LLParent.class, parentKey);
  assertThat(entity.getKey(), is(equalTo(parentKey)));
  assertThat(entity.getProperty1(), is(equalTo("parent:property1value")));
  assertThat(entity.getProperty2().getValue(), is(equalTo("parent:property2value")));
  assertThat(entity.getChildA(), is(not(nullValue())));
  assertThat(entity.getChildA().getProperty1(), is(equalTo("childA:property1value")));
  assertThat(entity.getChildBList(), is(not(nullValue())));
  assertThat(entity.getChildBList().get(0).getProperty1(), is(equalTo("childB1:property1value")));
  assertThat(entity.getChildBList().get(1).getProperty1(), is(equalTo("childB2:property1value")));
  pm.close();
}

low-level APIによる取得とassert

private void assertByLowLevelApi(Key parentKey) throws EntityNotFoundException {
  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  Entity entity = service.get(parentKey);
  assertThat(entity.getKey(), is(equalTo(parentKey)));
  assertThat((String) entity.getProperty("property1"), is(equalTo("parent:property1value")));
  assertThat(((Text) entity.getProperty("property2")).getValue(), is(equalTo("parent:property2value")));
  // 以下のふたつは自動的に取得されない…といぅか、属性すら存在しない。
  assertThat(entity.hasProperty("childA"), is(equalTo(false)));
  assertThat(entity.hasProperty("childBList"), is(equalTo(false)));
  // OneToOneのChildAを取得する。
  Query queryA = new Query(LLChildA.class.getSimpleName(), entity.getKey());
  Entity childA = service.prepare(queryA).asSingleEntity();
  assertThat((String) childA.getProperty("property1"), is(equalTo("childA:property1value")));
  // OneToManyのChildBを取得する。
  Query queryB = new Query(LLChildB.class.getSimpleName(), entity.getKey());
  List<Entity> childBList = service.prepare(queryB).asList(FetchOptions.Builder.withOffset(0));
  assertThat((String) childBList.get(0).getProperty("property1"), is(equalTo("childB1:property1value")));
  assertThat((String) childBList.get(1).getProperty("property1"), is(equalTo("childB2:property1value")));
}

これがlow-level APIでのRelationshipのクエリのサンプルです。low-level APIの保存の時にLLParentkindは子エンティティを保持するpropertyを持たない、と書きましたが、Entity#hasProperty()メソッドを使ってそれを検証しています。

検証する為のテストメソッド

@Test
public void JDOで保存してJDOで読む() {
  Key parentKey = saveByJDO();
  assertByJDO(parentKey);
}

@Test
public void JDOで保存して低レベルAPIで読む() throws EntityNotFoundException {
  Key parentKey = saveByJDO();
  assertByLowLevelApi(parentKey);
}

@Test
public void 低レベルAPIで保存して低レベルAPIで読む() {
  Key parentKey = saveByLowLevelApi();
  assertByJDO(parentKey);
}

@Test
public void 低レベルAPIで保存してJDOで読む() throws EntityNotFoundException {
  Key parentKey = saveByLowLevelApi();
  assertByLowLevelApi(parentKey);
}

これにより、以下の事が確認できます。

「JDOで色んなRelationshipの方法があるが、結局「EntityGroupはキーだけで構成される」という事がわかります。JDOで保存してlow-level APIで読み出すと、子エンティティを保持するpropertyが親エンティティからは完全に消滅しています。逆に、子エンティティを保持しない状態でlow-level APIで保存しても、JDOで読み出すとしっかり子エンティティが親エンティティに保持されています。これらはJDOが裏で色々とやってくれている、という事です。

ちょっとだけ裏を覗いてみたり

Datastoreサービスが実行されたタイミングをフックして、Datastoreサービスどのメソッド(機能)が実行されたかを標準出力に表示すると、次のような事がわかります。フックを行ったりするのに使用したソースはエントリの最後尾にあります。

JDOによる保存

makeSyncCall:datastore_v3:BeginTransaction
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Commit

プログラムからはPersistenceManager#makePersistent()を一回実行しているだけですが、実際はPUTメソッドが4回呼ばれています。

low-level APIによる保存
makeSyncCall:datastore_v3:BeginTransaction
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Put
makeSyncCall:datastore_v3:Commit

JDOと比べて、DatastoreService#put(List entities)の存在でPUTメソッドの呼び出し回数が2回で済んでいます。ちなみに、ひがさんが「次のバージョンの隠れた目玉機能としてparalel putが実装される」とおっしゃってたので、次期バージョンではJDOも同じ回数になると思われます。

読み出し

読み出しはJDOもlow-level APIも変わりません。JDOではPersistenceManager#get()を一回実行しているだけですが、実際にはGET、OneToOneをフェッチした時のRunQuery、OneToManyをフェッチした時のRunQuery、とlow-level APIと全く同じになります。

makeSyncCall:datastore_v3:Get
makeSyncCall:datastore_v3:RunQuery
makeSyncCall:datastore_v3:RunQuery

テストケースのsetup/teawdown部分

最後にこのテストケースで使用したDatastoreの初期化やら何やらの部分を書いておきます。ApiProxy#setDelegate()でちょっとだけ工夫をしています。

static PersistenceManagerFactory factory;

@BeforeClass
public static void setUpBeforeClass() {
  factory = JDOHelper.getPersistenceManagerFactory("transactions-optional");
}

@Before
public void setUp() {
  ApiProxy.setEnvironmentForCurrentThread(new ApiProxy.Environment() {
    public String getAppId() { return "unit test"; }
    public Map<String, Object> getAttributes() { return new HashMap<String, Object>(); }
    public String getAuthDomain() { return "gmail.com"; }
    public String getEmail() { return "hoge@gmail.com"; }
    public String getRequestNamespace() { return ""; }
    public String getVersionId() { return "1"; }
    public boolean isAdmin() { return false; }
    public boolean isLoggedIn() { return true; }
  });
  ApiProxy.setDelegate(new MyDelegate(new File("target/lowLevelApiTest")) {});
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString());
}

@After
public void tearDown() {
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  LocalDatastoreService datastoreService = (LocalDatastoreService) proxy.getService("datastore_v3");
  datastoreService.clearProfiles();
  datastoreService.stop();
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
}

static class MyDelegate extends ApiProxyLocalImpl {

  MyDelegate(File folder) { super(folder); }

  public byte[] makeSyncCall(Environment environment, String packageName, String methodName,
      byte[] request) throws ApiProxyException {
    if (packageName.equals("datastore_v3")) {
      System.out.println("makeSyncCall:" + packageName + ":" + methodName);
    }
    return super.makeSyncCall(environment, packageName, methodName, request);
  }

  public void log(Environment environment, LogRecord logRecord) {
    super.log(environment, record);
  }
}

余談(ここまでの話と全然関係がありません)

ここまで読んでしまうような変態な皆さんは、フックする為に使用しているMyDelegate#makeSyncCall()メソッドが気になって仕方無いですよね。特に第四引数であるbyte[] requestとかいうヤツ。「…何が入っているんだろう…というか全部が入っているんだよね?何でも取得できるよね」という予測ですよね!ズバリその通りです、ここに全部入っています。ただ、この中身は実行されるサービスによって変わる、ProtocolBufferによるシリアライズの結果です。例えば今回使ったDatastoreサービスのPUTメソッドであればcom.google.apphosting.api.DatastorePb.PutRequestオブジェクトに組み立てられます。Mailサービスならcom.google.appengine.api.mail.MailServicePb.MailMessage、といったような具合です。

説明すると長くなるのでここでは説明しませんが、これをうまく活用する事でローカル環境での自動テストが随分やりやすくなったり、テストの範囲が広がります。GAE/Jの中でも最も面白い部分かもしれません。

さらに余談の余談

SDKの中にはcom.google.apphosting.utils.remoteapi.RemoteApiServletといぅサーブレットが存在しています。こいつは、POSTメソッドの実装の内側でPostされたバイト配列を組み立ててサーバ側でmakeSyncCall()する仕組みになっています。…面白いですよね?…面白いはずなんですが、twitterでいっくらつぶやいても誰からも反応が無くて寂しかったりw

追記

「low-level APIによる取得とassert」のソースでChildBListのSort順についてコメントを書いていた箇所を削除しました。別の調査でJDOの@Orderアノテーションの動作との比較等を確認しようとしていたコードが残ってしまってました。