2009年12月28日月曜日

App Engine JDO Tips

Google App Engineのデータストアを利用する場合、公式にはJDOおよびJPAが提供されています。 過去にJDOからSlim3 Datastoreに乗り換えるようなことも書いていますが、最近のJDOについて少し書いてみたいと思います。

今回は、Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されている内容をもとに話を進めます。JDOの使い方自体に詳しくなくてもある程度読めるように書いたつもりですが、何かあればコメントください。

相互参照する one-to-many の関係 (episode 1, 3)

JDOを利用すると、相互参照を行うone-to-manyの関係を簡単に記述できます。

下記はParentクラスのオブジェクトが、複数のChildクラスのオブジェクトを保有する例です。

package com.example;

import java.util.*;
import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Parent {

    @PrimaryKey
    private String keyName;

    // Childクラスのparentプロパティが自身への参照を持つ
    @Persistent(mappedBy = "parent")
    @Element(dependent = "true")
    private List<Child> children = new ArrayList<Child>();

    // ...
}
package com.example;

import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.Key;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Child {

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    private String value;

    // Parentへの参照を保持して相互参照させる
    private Parent parent;

    // ...
}

上記のように書いた場合、ParentクラスのchildrenにChildクラスのオブジェクトを設定するだけで、自動的にそれぞれのChildクラスのparentフィールドにはParentクラスのオブジェクトが設定されます。

また、上記のような owned-relationship と呼ばれる関係は、ParentをルートとしたEntity Groupを自動的に構成します。このため、Childクラスのオブジェクトを持つParentはトランザクション処理による変更が可能です。

このEntity GroupはKeyのパスで表現されます。Childクラスのエンティティは親を持つことになるので、@PrimaryKeyの指定を、(String, Longなどではなく)Key型のフィールドに対してする必要があります。

なお現在の実装では、上記のようなParentを保存する際にchildrenというプロパティはデータストア上に作成されません。そのかわり、Childを保存する際にchildren_INTEGER_IDXというプロパティがデータストア上に作成され、Parentから見たChildの位置(children内のインデックス)が保持されます。

このため、過去のParentと同じキーを持つParentオブジェクトを生成し、childrenを操作してしまうと過去のchildrenの内容がマージされるという残念な状況になります。つまり、2つのChildオブジェクトを持つParentを2回作成すると、あとでParentを取得した際に4つのChildオブジェクトを持っていたりします。

そのため、上書きする場合でも一度取得したオブジェクトを利用します。

PersistentManager pm = ...;

Parent parent;
try {
    // すでに存在したら再利用
    parent = pm.getObjectById(Parent.class, "parent");
}
catch (JDOObjectNotFoundException e) {
    // 存在しなければ新しく作る
    parent = new Parent();
    parent.setKeyName("parent");
    pm.makePersistent(parent);
}

Child child1 = new Child();
Child child2 = new Child();

child1.setValue("child-1");
child2.setValue("child-2");

// ChildオブジェクトをParent.childrenに追加
List<Child> children = parent.getChildren();
children.add(child1);
children.add(child2);

JDO(とJPA)はtransparent persistence (透過的な永続化)を提供するためのライブラリです。上記のように新しく作成したオブジェクトはすぐにPersistentManager.makePersistent()によって永続化対象に指定し、そのオブジェクトを使い続けるスタイルのほうがやりやすいかもしれません。

キーのみを取得するクエリ (episode 4)

キーのみを取得するクエリが利用可能になっています。 やりかたは、JDOQLを書く際に"SELECT <キーを保持するフィールド名> FROM ..."のように、@PrimaryKeyの指定をしたフィールドを指定するだけです。

PersistentManager pm = ...;

// SELECT <キーを保持するフィールド名> FROM ... でキーのみを取得
Query query = pm.newQuery("SELECT key FROM " + Hoge.class.getName());
List<Key> keys = (List<Key>) query.execute();
...

App Engineのクエリは、(1)キーの一覧を取得、(2)キーからエンティティを取得、の順で結果を取得しているようなので、キーのみを取り出す場合には上記のように書くことで、(2)の処理をスキップすることができます。

エンティティの取得とモデルへのマッピングはそれなりにコストが高い操作ですので、キーだけが必要である場合には便利です。特に検索のみに利用するエンティティなどは、キーに検索結果としての有効な情報を凝縮することで、不要な情報をロードしなくて済みます。

シリアライズしたオブジェクトの格納 (episode 5)

App Engineのデータストアに利用できるデータの種類は限られていますが、JDOを利用すると自動的にオブジェクトのシリアライズを行ってBlobとして永続化できます (Blob StoreのBlobとは別物です)。

package com.example;

import java.util.Map;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class ObjectHolder {

    @PrimaryKey
    private String keyName;

    // MapはJDOで扱えないので、serializedを指定する
    @Persistent(serialized = "true")
    private Map<String, String> map;

    // ...
}

@Versionを利用したLong Running Transaction (episode 6)

App Engineのデータストアにはトランザクション処理を行うための仕組みがあらかじめ組み込まれていますが、リクエストごとにコミットまたはロールバックしなければならないため、画面をまたぐような長期間の並行性制御(Long Running Transaction)はソフトウェアで指示する必要があります。 JDOにはこれを自動的に行う仕組みが備わっています。

まず、JDO向けのモデルクラスに@Versionというアノテーションを付与します。

package com.example;

import java.io.Serializable;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
@Version
public class Versioned implements Serializable {

    private static final long serialVersionUID = 1L;

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    private String value;

    // ...
}

このとき、detachable = "true"という指定と、implements Serializableを併せて指定しておきました。前者はオブジェクトをJDOの管轄から外してスナップショットを作成する(デタッチ)のための指定で、後者はセッションに同オブジェクトを保持しておくための指定です。

Long Running Transactionを開始する場合には、下記のように@Versionを付与したモデルにPersistenceManager.detachCopy()を実行して記憶しておきます。

PersistenceManager pm = ...;
Versioned data = ...;
data.setValue("Hello!");
pm.makePersistent(data);
// 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
// snapshotをセッションなどに保存

それぞれの画面で行われた変更は、全て上記のデタッチしたsnapshotに対して行います。 全ての操作が終わったら、最後にデータストアに書き戻します。

PersistenceManager pm = ...;
Versioned snapshot = ...;
pm.makePersistent(snapshot);

上記のように、JDOの@Versionを利用したLong Running Transactionでは、明示的にJDOのトランザクションを開始する必要はありません。というより、明示的にJDOのトランザクションを利用すると、@Versionによる並行性制御が無視されます

これはそれなりに有名なことらしいのですが、知らないと非常に不可解な動きをします。このエントリを書いてる最中に同じようにはまって、様々な方に助けていただきました(いつもありがとうございます)。

このLong Running Transactionは、PersistenceManager.close()を実行した際に実際の反映が行われます。もし、スナップショットを書き戻すまでに外部から(本体の)モデルが変更されている場合、close()が実行されるタイミングでJDOExceptionがスローされていました。

このPersistenceManager.close()の中では、おそらく下記のような操作が実行されています。

PersistenceManager pm = ...;
SomeModel newData = ...;
...
Transaction tx = pm.currentTransaction();
try {
  // 現在のトランザクションを利用して、同じIDのオブジェクトを取り出す
  SomeModel oldData = pm.getObjectById(SomeModel.class, newData.getId());
  // サーバから取り出したバージョンと、手元のバージョンを比較
  // 他の人が変更していたらサーバから取り出したバージョンがずれているはず
  if (oldData.getVersion() != newData.getVersion()) {
    // 実際にはJDOExceptionがスローされる
    throw new ConcurrentModificationException();
  }
  // バージョンチェックと書き込みは同一トランザクションで
  // 処理するのでアトミックに行われる
  pm.makePersistent(newData);
  tx.commit();
}
catch () {
  if (tx.isActive()) {
    tx.rollback();
  }
}

より詳しい考察は、App Engineでバージョンによる楽観的排他制御 - ひがやすを blogなどでも行われています。

はまった点

@Versionの調査をしていてひどくはまっていた点を紹介します。

PersistenceManager pm = ...;
Versioned data = ...;
// (a) 先にmakePersistent()で永続化の対象に指定
pm.makePersistent(data);
// (b) そのあとにモデルを変更
data.setValue("Hello!");

// (c) 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
...

// (d) 最後にclose()
pm.close()

上記のように、最初にPersistenceManager.makePersistent()を実行(a)し、それ以降にモデルオブジェクトを変更した(b)場合、最後のPersistenceManager.close()の瞬間(d)にモデルオブジェクトのバージョンが変更されます。このため、(b)と(d)の間で行われたスナップショットの作成(c)は、(d)で変更されるバージョンの情報を反映していません(aの時点のバージョンを利用します)。

このようなスナップショットの書き戻しは常に失敗します。これは(d)の時点で変更されたバージョンが「Long Runnning Transactionの外部で変更が行われた」というように見なされてしまうためです。

makePersistent()detachCopy()を同一リクエストで行う場合、最初に紹介したように「すべて変更してからmakePersistent()を実行する」というようにした方がよさそうです。

PersistenceManager pm = ...;
Versioned data = ...;
// 先に変更してから
data.setValue("Hello!");
// makePersistent()で永続化の対象に指定
pm.makePersistent(data);
// すると、detachCopy()はmakePersistent()時点のバージョンと一致
Versioned snapshot = pm.detachCopy(data);

transparent persistence (透過的な永続化)とさっき書いたばかりなのですが、App Engineではどうもそう簡単にいかないようです。

インデックスを作成しないプロパティ (episode 7)

データストアにエンティティを格納した場合、基本的にはそれぞれのプロパティに対するインデックスが作成されます(TextやBlobなどは対象外)。しかしこのインデックス作成はCPUやデータストア領域などのリソースを消費するため、不要の場合には抑制させることもできます。

具体的には、インデックスの作成を抑制したいプロパティに対して、@Extension(vendorName="datanucleus", key="gae.unindexed", value="true")という長い記述の注釈を付与するだけです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class BlogEntry {

    @PrimaryKey
    private String keyName;

    // title はインデックスを作る
    private String title;

    // permalink はインデックスを作らない
    @Persistent
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

上記のモデルに対してpermalinkをフィルタに指定するクエリを実行した場合、クエリは失敗するのではなく単に結果を何も返しません

なお、@Extensionにはのほかにorg.datanucleus.jpa.annotations.Extensionという注釈型も存在します。後者はJPA用なので注意が必要です (Enhanceの際にエラーが表示されます)。

インデックスの作成については、How Entities and Indexes are Stored - Google App Engine - Google Code (英語)が参考になると思います。

はまった点

今回のはまりは下記のような感じです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class BlogEntry {

    @PrimaryKey
    private String keyName;

    private String title;

    // @Persistent を省略
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

プロパティに@Persistentを省略した場合、永続化対象にはなるけど"unindexed"の対象にならないという残念な挙動をします。このエントリでは基本的に@Persistentを省略していますが、可能な限り明記したほうがいいのかもしれません。

IN, != 演算子の追加 (episode 9)

SDK 1.2.8 Release Notesで語られなかったことでも書きましたが、SDK 1.2.8からはIN, !=という演算子が利用できるようになりました。下記のようにJDOQLの中で指定します。

Query query = pm.newQuery(Employee.class);
// 年齢が20歳でない
query.setFilter("age != 20");
List<Employee> results = (List<Employee>) query.execute();

...
// 特別な年齢の一覧
List<Integer> anniversary = Arrays.asList(20, 40, 60);
Query query = pm.newQuery(Employee.class);
// 年齢が「特別な年齢の一覧」に含まれる
query.setFilter(":anniversary.contains(age)");
List<Employee> results = (List<Employee>) query.execute(anniversary);

...

その他、気付いたこと

Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されなかったものの、App Engine for Javaのリリース当初から変わっていたことについて、いくつか紹介します。

PersistenceManagerFactoryの初期化が高速化

Slim3 Datastoreに乗り換えるという記事では、JDOをやめた理由の一つに「初期化が遅い」ということを挙げました。 しかし、SDK 1.2.8より利用可能になった<precompilation-enabled>true</precompilation-enabled>という設定により、この初期化のコストをかなり削減できるようです。

このプリコンパイル設定をonにしたりoffにしたりして、それぞれの初回アクセス時の負荷を測定してみました。いずれもPersistenceManagerFactoryを初期化し、pmf.getPersistenceManager()を起動するだけの簡単なプログラムです。

プリコンパイル設定なし プリコンパイル設定あり 削減された時間
1回目 4433cpu_msec 2450cpu_msec 1983cpu_msec
2回目 4394cpu_msec 2741cpu_msec 1653cpu_msec
3回目 4588cpu_msec 2585cpu_msec 2003cpu_msec
4回目 4725cpu_msec 2741cpu_msec 1984cpu_msec
5回目 4510cpu_msec 2605cpu_msec 1905cpu_msec
平均 4530cpu_msec 2624cpu_msec 1906cpu_msec

上記のように測定回数は5回と多少心もとないですが、いずれも2000cpu_msec近い時間がプリコンパイル設定を有効にすることで削減されています。 体感的にも応答に5秒かかっていたものが3秒くらいになっている感じがしますので、上記のプリコンパイル設定は有効にする価値がありそうです。

コアデータタイプがデフォルトフェッチグループに追加

過去のJDOでは、次のようなモデルクラスを作成した際に、linkフィールドやmailsフィールドにはデフォルトフェッチグループの指定が必要でした。 デフォルトフェッチグループとは、特別な指定をせずにエンティティをデータストアから読み出した際に、自動的にフェッチされるプロパティ群のことです。 プリミティブ型、String型、Date型などはJDOの定義でデフォルトフェッチグループに含まれていましたが、App Engine特有のクラス(Key, Link, Email)などはそれらに含まれていなかったため、わざわざフェッチグループの指定をしないとデータストアからロードした際にnullのまま放置されていました。

package com.example;

import java.util.List;
import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class CoreDataType {

    @PrimaryKey
    private String keyName;

    private Link link;

    private List<Email> mails;

    // ...
}

SDK 1.2.8 (datanucleus-appengine 1.0.4)からは com.google.appengine.api.datastore.* にある組み込みデータ型や、それらのコレクションについてもについてもデフォルトフェッチグループに追加されるようになったので、上記のモデルクラスはフェッチグループの指定なしに利用できます。

2009年12月18日金曜日

Google AppsアカウントでGAEが利用可能に。

気づくと、GAEのTOPページにこのような記述が増えていました。
Google Apps アカウントで Google App Engine にログインする場合は、次の URL を使用してください。 https://appengine.google.com/a/<YOURDOMAIN.COM>/
Google Appsアカウントで試してみました。 開発者として招待してみると、Googleアカウントで利用する際と違い、招待メールの内容にGoogleアカウントの承認リンクとは別にGoogle Appsアカウント用の承認リンクが追加されています。
To sign in with your Google Account and accept or decline this invitation, simply follow this link:
<Googleアカウント用承認リンク>
OR, if <メールアドレス> is a Google Apps Account, you can sign in and accept or decline this invitation here:
<Google Appsアカウント用承認リンク>
Google Appsを導入していて、これからGAEでの開発やデプロイして利用等には、アカウントの統一という面で利用する価値はありますね。

2009年12月17日木曜日

Google App Engine Downtime Notify でGAEのダウンタイムを知る。

 GAEを利用したアプリケーション開発・保守に関わっていらっしゃる方であれば、GAEへのデプロイ、テスト、運用時の障害というのは気にかかるところです。
 Google App Engine Downtime Notify ではメンテナンスによる事前のスケジュールも含めて、Googleからのダウンタイムの通知が行われており、このグループのメーリングリストに参加すると、Googleからの通知専用のメールが配信されます。
 配信時刻とダウンタイムの時間が太平洋標準時のため、日本時間の16時間前となることと、メンテナンスの予定の通知以外は、ダウン後のダウンタイムの通知になってしまうことについては注意が必要です。
※過去も投稿の時刻を読む限り、サマータイム時についても、そのままの時間で配信されているようです。
 最近では、12月3日の9:33(太平洋標準の12月2日16:33)に「Index definition update issue today」という投稿が配信されています。
「太平洋標準時でおよそ12:55からPM3:55の間に、App EngineのデータストアのIndex definitionsを変更しました。それによるアプリケーションやユーザーへの影響はありませんでしたが、開発者がアプリケーションのデプロイや、Index definitionsを更新しようとするとすると"500 エラー"が発生していたでしょう。この問題は現在、解決され、開発者はアプリケーションのデプロイやIndexの更新が出来る筈です。」
 と、原因や現在の状況をお知らせしてくれますので、何か(無いに越したことはありませんが)と助かるかと思います。GAEのメール受信機能を利用して障害報告のお知らせ画面を作成されている方も居るんじゃないでしょうか。
 また、今まさに障害かな?と感じた場合はGAE/Jの日本海外のフォーラム、またはGAEのフォーラムを覗いてみると、他の開発者が書き込んでいるかもしれません。

2009年12月16日水曜日

GData APIの認証に触れてみる

Webアプリケーションを開発する際に、Googleを始めとした様々なWebのサービスを利用する機能を実装することが多くなってきたのではないでしょうか。その際に必要なAPIが各サービスプロバイダから公開されているケースが多く、手軽に試すことが可能です。
 GoogleのGData APIでは、Java、JavaScript、.Net、PHP、Python、Objective-Cといった様々な言語で開発するアプリケーションから、Googleのサービスのデータを利用出来ます。
http://code.google.com/intl/ja/apis/gdata/docs/client-libraries.html
 業務アプリケーションで、Google Apps上のGoogle Calendarと連携を行うといったケースは十分に考えられますし、Google Docsのデータを用いて最近、話題になっているGoogle Visualization APIを利用しグラフを表示するといった面白そうな事例も今後、増えてくるのではないでしょうか。
 一般にこうしたアプリケーションを開発する際に、各サービスの認証を考慮することは避けては通れません。
 例えば、開発対象のアプリケーションを利用してユーザーがGoogle Calendarの自分の予定を閲覧する、あるいはBloggerの記事を投稿するといったことを実現する場合、ユーザーに自分のアカウントを認証してもらう必要があります。
 GData APIでは認証する際に四つの方法が用意されています。
  • Authentication for Web Applications: AuthSub
  • Authentication for Web Applications: OAuth
  • Authentication for Installed Applications: ClientLogin
  • Authentication for Gadgets
 上記リンク先の利用目的で分類されているように、WebアプリケーションではAuthSub、OAuth、インストールを要するデスクトップアプリケーション等ではClientLogin、ガジェットからはOAuth Proxyを用いて認証を行うことが示されています。
 ClientLoginではアプリケーション上でユーザーにGoogleアカウントの情報(ユーザーIDとパスワード)を登録してもらい、それを利用して認証を行います。
 Webアプリケーションの場合、ClientLoginのように直接、アプリケーション上でユーザーIDとパスワードを登録してもらうのではなく、別の方法が推奨されています。Google独自のAuthSubと、API認可プロトコルの標準仕様てして策定されているOAuthです。
 これらの認証方法はユーザーから見ると、特定の第三者となるアプリケーション側にパスワードを渡さないことから、セキュリティ上のリスクが低くなると考えられます。
 私の場合、Googleのサービスの認証以前に認証技術そのものが、まだまだ判っていないため、ドキュメントやWebの情報を読むのにも一苦労と勘違いの連続です(^^;こちらの記事は大変、参考になりました。
 AuthSubでは、アプリケーションがGoogleアカウントのサイト上へ認証を要求します。ユーザーがGoogleアカウントのサイト上で認証を行い、認証に成功すると、次に、アプリケーションがユーザーのアカウントのデータにアクセスすることを承認するかどうかを確認します。そして、ユーザーが承認した場合、アプリケーションへリダイレクトされます。この際にパラメーターにGoogleアカウントにより承認された認証トークンが付与されており、これを利用することでアプリケーションはユーザーのデータを参照することが出来ます。
 ClientLogin以外の認証を実現する場合、理解もさることながら実装の手順も複雑ですが、Google Code上にサンプルがあるので非常に助かります。
 AuthSubによる認証のサンプルが用意されています。このサンプルではGoogle Calendarのデータを参照します。
 ※サンプルを実行する場合、以下のjarファイルが必要ですので、パスを通す必要があります。
  • gdata-appsforyourdomain-1.0.jar
  • gdata-base-1.0.jar
  • gdata-calendar-1.0.jar
  • gdata-client-1.0.jar
  • gdata-core-1.0.jar
 ダウンロードはこちらから可能です。gdata-samples.java-1.40.1.zip を選択すると、AuthSubのサンプルも含めたファイルとjarファイルが入手出来ます。 gdata-src.java-1.40.1.zip の中身はソースとjarファイルです。
 ちなみにリリースノートによると、1.40からGoogle Translator Tookkit Data API が利用出来るようになったとのことなので、こちらも楽しみです。
 注意点として、このサンプルをEclipse上でGAE/Jのアプリケーションとして試す場合は、セッションを有効にするため、WEB-INF以下の appengine-web.xml に下記の記述を追加する必要があります。
<sessions-enabled>true</sessions-enabled>
 サンプルの実行画面は下記となります。
 サンプルの sample/authsub/web以下にある index.jsp が sample/authsub/src/LoginServlet を呼び出し、認証がされていなければGoogleのログインページへ遷移され、アプリケーションからカレンダーのデータを参照するかどうかの確認画面が表示されます。
 認証が既に行われていれば、sample/authsub/web以下の main.jsp が表示されますので、認証されたアカウントのGoogle Calendarのデータが参照出来る筈です。
 main.jsp 内ではJavaScriptを利用して、Google Calendarのデータを参照しています。JavaによるGoogle Calendarを利用したサンプルもこちらで見ることが出来ます。認証方法がClientLoginになっていますが、カレンダーへのデータの登録、削除、参照が実装されているようなので、GData APIを利用する参考になるかと思います。ダウンロード可能なサンプルは上記を含め、他のサービスの利用例が確認出来ます。
 手前味噌になりますが、AuthSub認証によるGData APIの利用については、弊社の書籍でP.269からP.276にソースと解説が掲載されています。
 
 また、Webアプリケーションで推奨されるGDataへの認証方法として、もう一つOAuthがあります。OAuthではAuthSubより複雑なやり取りになります。
  • アプリケーション側はGoogleアカウントのサイト上へ認証を要求する前に、リクエストトークンと呼ばれるキーを要求します。この時点で、Googleから渡されるリクエストトークンは未承認の状態です。
  • アプリケーションは未承認のリクエストトークンをパラメータに付与し、Googleアカウントのサイト上へ認証を要求します。
  • Googleアカウントのサイト上で、ユーザーがGoogleアカウントのサイト上で認証を行います。
  • アカウントの認証に成功すると、ユーザーはアプリケーションがアカウントのデータにアクセスすることを承認するかどうかをGoogleアカウントのサイト上で決定します。ユーザーが承認した場合、アプリケーションへリダイレクトされます。この際、パラメーターに認証が成功したことを示すGoogleにより承認されたリクエストトークンが付与されます。
  • アプリケーションは承認されたリクエストトークンを用いてアクセストークンというキーと交換することを要求します。Googleにより交換されたアクセストークンが発行されます。
  • アプリケーションは受け取ったアクセストークンを利用することで、ユーザーのデータを参照することが出来ます。
 ※OAuthの解説によると、データを提供するサービスをサービス・プロバイダー、サービスを利用するアプリケーションをコンシューマと位置づけています。gihyo.jpさんのこちらの記事がとても参考になります。
 この手順の中で、アプリケーション毎のコンシューマーキー、コンシューマーシークレットというキーに加えて、アプリケーションからGoogleへのリクエストが署名されている必要があります。こちらに詳細が記されています。
 また、OAuth_Playgroundというサイトでは、Googleのサービスを利用するリクエストトークンの生成、承認、アクセストークンの取得といったプログラム上で行うべき手順を体験出来ますので、ドキュメントと併せて利用すると参考になるかと思います。
 Google Code上ではOAuthによるGData API利用のサンプルがありますので、試してみてはいかがでしょうか。また、GAE/J上でOAuthによるGData API利用のサンプルがこちらでデモされています。ソースはGoogle Code上のこちらとなります。
 最後に追記しますと、GAEで開発を行う場合、特にGoogle Appsとの連携が要件としてある場合は、ユーザーのロールと認証が複数存在するので、頭の中で整理をしておかないと混乱を招くかもしれません。当Blogのこちらの記事を読まれると理解の一助になるのではないかかと思います。
 (12/17 PM3:15 追記) Twitter上でご指摘を頂いた通り、上記のAuthSubのサンプルをGAE/J上で実行される際に、appengine-web.xml に設定を追加する必要があります。こちら、または、こちらをご確認下さい。どうもありがとうございました。

2009年12月15日火曜日

AppEngine1.3.0 Blobstore API入門

AppEngine1.3.0で新しいAPIとして50MBまでのファイルを登録することが出来るBlobstore APIが公開されました。今までは10MBのアップロード、Datastoreには1MBずつしか登録出来なかったので、これは非常に期待がもてるAPIです。折角なので公式ドキュメントを参考に一通り試してみました。

Blobstoreとは?

BlobstoreはDatastoreの厳しいサイズ制限を超えて50MBまでのファイルを登録し、利用することができるサービスです。(Billingを有効にしていないと利用することが出来ません。)このサービスは、Datastoreの様に直接アクセスして利用するのではなく、サービスの受け口に対してWebフォームや、HTTP POSTメソッドによって行われます。従って、アップロード/ダウンロードにかかる処理時間はAppEngineの30秒制限の影響受けることなく利用することが可能なようです。但し、これは検証してないので正確には言えませんが、アプリケーションの中で利用するケースには当てはまらないと思います。

早速試してみる

公式ドキュメントのサンプルコードをそっくりそのまま試してみました。まずはアップロード用のフォームをもったJSPから。
<%@ page language="java" contentType="text/html; charset=UTF-8"
 pageEncoding="UTF-8"%>
<%@ page import="com.google.appengine.api.blobstore.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
String uploadUrl = service.createUploadUrl("/upload");
%>
<form action="<%=uploadUrl%>" method="post" enctype="multipart/form-data">
    <input type="file" name="myFile">
    <input type="submit" value="Submit">
</form>
</body>
</html>
ここではformのactionに対して、BlobstoreService#createUploadUrl(String)を使ってアップロード先となるURLを生成して指定しています。データのアップロードはここで生成されたURLに対して行われます。登録時に必要なその他の処理は、引数で指定している"/upload"にマッピングされるサーブレットなどで行います。それでは次に、"/upload"にマッピングされるサーブレットを見てみましょう。
public class Upload extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
        Map<String, BlobKey> blobs = service.getUploadedBlobs(req);
        BlobKey blobKey = blobs.get("myFile");
        if (blobKey == null) {
            resp.sendRedirect("/");
        }
        else {
            resp.sendRedirect("/serve?blob-key=" + blobKey.getKeyString());
        }
    }
}

このサーブレットでは、リクエストからアップロードされたデータに対するキーとなるBlobKeyを取り出し処理を行います。実はこのサーブレットが呼び出される前に既にデータのアップロードは完了しています。従ってここでの処理は登録された後に行われます。アップロードされたファイルの妥当性のチェックを行い、削除するなどの処理を行う必要があります。 最後に、上記のUpload.javaでは""/serve?blob-key="+ blobKey.getKeyString()"に対してアクセスし、アップロードしたファイルをダウンロードするサーブレットにリダイレクトしているので、こちらも見てみましょう。
public class Serve extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
            IOException {
        BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
        BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
        service.serve(blobKey, resp);
    }
}
名前が若干ややこしくて使う際にちょっと疑問に思いましたが、BlobstoreService#serve(BlobKey, HttpServletResponse)メソッドを利用して、レスポンスにBlobデータを"Serve"することが出来るようです。ここは想像なのですが、30秒制限の影響を受けることなく利用するこが出来るように、レスポンスに書き出す処理は直接行うのではなく、アップロード時と同じように間接的に処理されているはずです。

アップロードしたファイル情報の取得

登録されたデータに関する情報は、下記のようなコードで確認することができます。
BlobInfoFactory factory = new BlobInfoFactory();
BlobInfo blobInfo = factory.loadBlobInfo(blobKey);
BlobKey key = blobInfo.getBlobKey();
String contentType = blobInfo.getContentType();
Date creation = blobInfo.getCreation();
String filename = blobInfo.getFilename();
long size = blobInfo.getSize();
BlobInfoFactoryにはこの他にqueryBlobInfos()メソッド、queryBlobInfosAfter(previousBlob)といったメソッドで指定のBlobKey以降のものを取得することが出来るようです。現時点ではBlobInfoFactoryからはその他のプロパティに対するクエリをかけることは出来ない様です。しかし、BlobInfoFactoryの定数にカインド名やプロパティ名が定義されているのでそれらを使って下記のように取得することは可能です。
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
for (Entity entity : datastoreService.prepare(new Query(BlobInfoFactory.KIND))
    .asIterable()) {
    BlobInfo info = factory.createBlobInfo(entity);
    // 処理
}

アップロードしたファイルに対する関連

また、エンティティに紐付けて利用するケースもあるかと思います。その際BlobKeyがコアタイプとして利用することができるので、該当のエンティティのプロパティに持たせてあげることによって実現出来ます。試しに登録したほんとに簡単なサンプルが以下になります。
Entity entity = new Entity(KeyFactory.createKey("test", 1));
entity.setProperty("blobKey", new BlobKey("対象のblobKey"));
DatastoreService service = DatastoreServiceFactory.getDatastoreService();
service.put(entity);
このエンティティをDatastore Viewerで見てみると以下のように見えます。

view blobなんていうリンクがあるので、こちらをクリックしてみると、以下のようにイメージが表示されるようです。


まとめ

大容量のファイルを扱えるようになったことは非常に喜ばしいことですが、少なくとも現状ではこれらのデータは静的リソースとしての扱いにとどまる程度と思われます。アプリケーションが利用するファイルとして扱うのは現実的ではありません。(URLFetchする以外に方法はなさそうなので)とはいえ、このBlobstore APIによって動画、写真、音楽ファイルなどの利用が可能になるのは大きいでしょう。

Images APIとの連携(追記)

コメント欄にてtmatsuoさんに教えていただきました。ありがとうございます。
公式のImages APIのドキュメントがこっそり更新されていたようです。これによると、以下の様なコードでBlobstore上の画像ファイルを加工する事が出来るようです。但し、Images APIの制限である1MBまでの制約は残るので用途はドキュメントにもあるようにサムネイルの作成といったところでしょうか。
BlobKey blobKey;  // ...

ImagesService imagesService = ImagesServiceFactory.getImagesService();

Image oldImage = ImagesServiceFactory.makeImageFromBlob(blobKey);
Transform resize = ImagesServiceFactory.makeResize(200, 300);

Image newImage = imagesService.applyTransform(resize, oldImage);

byte[] newImageData = newImage.getImageData();

App Engine SDK 1.3.0 (overview)

先ほどApp Engine SDK 1.3.0がリリースされました。 新機能について簡単に紹介します。

Blobstoreサービス

今回のアップデートの最大の目玉は、Blobstore APIが追加された点です。

これまではApp EngineのDatastoreを利用していましたが、これに格納できるそれぞれのデータは1MBという厳しい上限があったため、巨大なデータを格納する用途には向いていませんでした(巨大なデータを格納するには、データを分割したり、他のサービスと連携したりしていました)。 今回追加されたBlobstoreは巨大なデータ(Binary Large OBject)を格納するためのAPIで、50MBまでのデータを取り扱えるようです。

ただし、APIを見る限りでは、ファイルの内容を直接アプリケーション内で操作する用途ではなく、現時点ではアップロードされたファイルの保存と、そのファイルをそのままクライアントに返す機能だけを提供しているようです。動的に追加削除ができるリソースファイルのような扱いでしょうか。

なお、このBlobstoreサービスは、現時点では実験的なサービスとして提供されています。また、利用にはBillingの設定が必要であるようです。細かい使い方などは、追って紹介していく予定です。

また、この変更に合わせてアプリケーションの管理コンソールに「Blob Viewer」という項目が追加されています。アップロードされたファイル名やファイルサイズ、更新日時などが確認でき、手動で削除することもできるようになっています。

Blobstoreの詳しい使い方については、こちらのエントリを参照してください。

動的言語の高速化

Javaはいくつかの動的言語をサポートしていて、RubyやGroovyなどJava以外の言語で書かれたプログラムをApp Engine上のJava VMで動作させることができます。 これらの動的言語はJavaのリフレクションAPIを多用するため、どうしてもパフォーマンスに多少難があるという問題がありますが、今回のアップデートでこのリフレクション周りの処理が改善されたようです。

公式の発表によると、今回の改善でリフレクションに関する処理が最大で10倍程度速くなるそうです。 もしかすると、いくつかのリフレクションを利用しているフレームワークも高速化されているかもしれません。

システムプロパティの追加

現在稼働しているアプリケーションの状態を知るために、便利なシステムプロパティが追加されています。

// アプリケーションのバージョン (appengine-web.xmlで指定したもの)
String version = System.getProperty("com.google.appengine.runtime.version");

// 実行環境 ("Production", "Development")
String environment = System.getProperty("com.google.appengine.runtime.environment");

これまでは、実行環境によって動作を変えるときには少々トリッキーなコードを書いていましたが、上記のプロパティ情報を確認するだけで判別できるようになりました。

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がスローされることを確認しました。

2009年12月11日金曜日

Eclipse PluginではじめてのGWT UiBinder

Eclipse のGoogle Plugin の 1.2.0.v200912062003 ではGWT2.0の開発がサポートされています。
 GWT2.0の新機能であるUiBinderを利用した開発支援機能も用意されており、お手軽に試すことが可能です。Eclipse3.5.1(Java6)で実際に動かしている様子を紹介してみたいと思います。
 ※Eclipse3.5でのインストール方法についてはURLのパスを3.5に変更するだけでOKです。また英語のサイトでは情報が更新されています。
 まず、Eclipse上で新規のプロジェクト作成を行います。メニューから「ファイル」→「新規」→「Webアプリケーションプロジェクト」を選択し、「Google Webツールキットを使用します」にチェックを入れて作成します。
 (パッケージ名).clientと(パッケージ名).server以下にGWTのサンプルコードが生成されます。但し、こちらはUiBinderを利用してません。ですので、新たにウイザードを起動します。プロジェクト・エクスプローラー上の(パッケージ名).client上で「ファイル」→「新規」→「UiBinder」を選択します。
 「generate sample contents」にチェックが入った状態のまま、名前を登録して「完了」をクリックすると、下記の画像のように(ファイル名).ui.xmlとファイル(ファイル名).javaの二種類のファイルが生成されます。
 ASP.NetのWeb Formのコードビハインドとなんだか似ていますね。*.aspx と *.aspx.cs(C#の場合) が*.ui.xml と *.javaに対応するようなイメージでしょうか。
 *.ui.xml側でUIの定義を行います。タグ内でCSSライクなスタイルの指定が出来ます。作成される雛形ではボタンが一つだけ定義されています。g:Buttonのタグ内で ui:field="button" と記述されており、これが*.java側の @UiFieldアノテーションのButton button; に対応しています。
 尚、Javaのエディタ上で*.ui.xmlにUIの定義が無いコンポーネントを記述すると下記の画面の様に警告をしてくれる補完機能が備わっているので、単純なスペルミスは回避出来るのではないでしょうか。
 残念ながら、.ui.xmlに定義を追加してもJavaには反映されないようです。*.ui.xml と *.java の2WayなWYSIWYGエディタが欲しいですね。
 また、ui.xmlとJavaのファイルの紐付けは同じパッケージ内の同じファイル名であれば、必要ないようです。もしファイル名が異なる場合は、Javaのソースに定義されているinterface上にアノテーションで @UiTemplate("ファイル名.ui.xml") と定義すれば紐付けされます。
 次に、この雛形をまず実行してみたいので、エントリーポイントとなるJavaのファイル、エントリーポイントを指定するモジュールの*.gwt.xmlファイル、HTMLファイルを作成します。
 こちらもウィザードが用意されています。メニューの「ファイル」→「新規」から「エントリーポイント」、「モジュール」、「HTMLページ」が各々、作成出来ます。
 エントリーポイントの*.javaファイルにはonModuleLoad() の空のメソッドが作成されていますので、UiBinderで作成した*.javaファイルのインスタンスをRootPanelに追加します。この作業は従来のGWTと同じですね。
 次に作成したエントリーポイントの*.javaファイルに対するモジュールを作成します。この際に、Inherited modulesを追加する必要があります。下記の画面の様に com.google.gwt.uibinder.UiBinder を指定して追加します。
 生成された*.gwt.xmlファイルにエントリーポイントの*.javaファイルを記述します。
 次いで、プロジェクト・エクスプローラー上の*.gwt.xmlファイルを選択した状態で対応するHTMLページを追加します。そしてWebアプリケーションの実行を行い、ブラウザ上で確認します。
 Google Pluginの機能が今後、充実してくれば、先行のFlex BuilderやVisual Studioの様にUIとコードが分離した状態での開発が一層、楽になるのではないでしょうか。
 P.S GWT2.0のリリースが asahi.com で紹介されていました。 GoogleのGWT2.0に対する力の入れ具合?が伺えます。

2009年12月10日木曜日

appengine-java-sdk-1.2.8-正式版

appengine-java-sdk-1.2.8が正式版としてリリースされているようです。Eclipse3.5で確認したところ、http://dl.google.com/eclipse/plugin/3.5 を指定してソフトウエア更新を行なうとプラグインとSDKがインストールされます。(こちらの記事にあるGWT.2.0.のSDKもインストールされます。)
追加・変更内容についてはプレリリース時の記事を参照していただくとして、その他に気になった点としては以下の3箇所でしょうか。

- New support for application pre-compilation to reduce the length of loading requests. To enable pre-compilation on your application, add this flag to your appengine-web.xml: <precompilation-enabled>true</precompilation-enabled> If you have trouble deploying your application, you should remove this flag or set it to false.
この設定を有効にすると、アプリケーションのデプロイ時に”Precompiling.”とログに表示されるようになります。 Google App Engine Blog の記事 によれば、この pre-compilation によりGAE上に新しくインスタンスが生成される時やスケールされるときの速度が改善されるようです。ただし、サンプルアプリケーション程度の規模では効果は実感できませんでした。

- Added Quota API (com.google.appengine.api.quota) to match Python API.
現状ではJavaDocと、公式ドキュメントのこちらくらいしか資料が見当たらないのですが、リクエストサイクル中のCPUの占有時間に関する情報が収集できるようです。

- Low-level Memcache API now supports batchIncrement().
インストールされたSDKのディレクトリにあるリリースノートでは batchIncrement() となっていますが、最新版では、 incrementAll() となっています。

GWT2.0リリース

Google Web Toolkit 2.0 のリリースに伴ない、Eclipse Plugin も 1.2.0.v200912062003 になりました。
 Eclipse3.5でインストールしてみたところ、1.7系まではローカル実行用のホスト・ブラウザを利用することでデバッグ中の画面を確認することが出来ましたが、2.0からはIn-Browser Development Modeという通常のブラウザを利用してデバッグする方法が用意されています。
 ブラウザにはプラグインをインストールする必要があり、IE、FireFox、Safari用のプラグインは既に用意されています。Google Chromeも4以降に向けて準備しており、Early Accessが利用出来るようです。
 Eclipseでの実行時にDeelopment Modeのビューが開き、ブラウザへのプラグインのインストールを推奨するメッセージが表示されます。
 メッセージには「このURLをブラウザへコピーして下さい。」とあり、ブラウザ上でプラグインの有無を判断して、下記のFirefoxでの画面例の様にインストールの案内が表示されます。
  試しに、IE8とFirefox3.5.5にインストールしてみました。例えば、Sample001.htmlというページを作成してデバッグを行うと、下記の様なURLで実行されています。
 http://localhost:8888/Sample001.html?gwt.codesvr=(実行しているPCのIPアドレス):9997
 実行中の様子はブラウザ毎にEclipse内のDevelopment Modeのビューで確認することが出来ます。例えば、FirefoxとIEの両方で実行する場合はビュー上に下記の様に表示されていますので、切り替えて別々に動作を確認します。
  • Sample001
  • -Sample001.html - IE
  • Sample001
  • -Sample001.html - FF
 尚、デバッグを停止している状態でブラウザのページにアクセスすると"GWT Code Server Disconnected"とお知らせしてくれて、なかなか?親切なようです。
 また、GWT2.0そのものも様々な新機能がアナウンスされていました。なかでも個人的に面白いと思う機能はUI Binderで、これはXMLでUIを定義出来ます。
 Adpbe FlexのMXML、MicrosoftのXAML を想起しますね。
 Google CodeのGWTのページでUI定義のサンプルが公開されています。Eclipse Plugin ではUI Binderの雛形の(作成したページ名).ui.xml と (作成したページ名).java のファイルが作成されます。
 UI上のコンポーネントを定義する際に、従来はJavaによるコーディングが必要で、面倒だという声もありましたが、UI Binderを利用することでそうした声の不満を解消することが出来るようです。

2009年12月8日火曜日

グルージェントWebページの裏側 (2)


弊社WebサイトはGoogle Bloggerと統合し、bloggerへの記事がマスターデータとしてGAEのデータストアにレプリケーションしています。弊社サイトのメインメニューとフッターメニューは管理画面で編集ができるようになっていますし、GWTのコンポーネントとして独立できるように実装しているのですが、残念ながらbloggerに使うことができていませんでした。

クロスサイトスクリプティングになっちゃうよ問題

弊社サイトが www.gluegent.com、Bloggerが songofcloud.gluegent.com となっているため、Webブラウザのクロスサイトスクリプテイングのセキュリティ制限により、GWT側からblogger側のURLを操作することができなかったのです。
それではと、GAEのモジュールを展開してblogger側に配備することを試してみました。しかしGAEのビルド済みモジュールは複数のHTMLリソースに分割されていて、URL上の同一階層に配置されていることを前提にダイナミックにロードする仕組みになっていました。bloggerは機能として、その構造内にGAEのモジュールなどの任意のリソースを配置できる仕組みにはなっていないため、やはりGAEコンポーネントを配置したwww.gluegent.com内のHTMLページをiframeを使ってガジェットのようにblogger側に取り込まなければならないということになります(※1)。

しかし実は今日から songofcloud.gluegent.comのメインメニューとフッターメニューは www.gluegent.comと同一のGAEコンポーネントを使って構成することが出来るようになりました。これで、管理画面からメニュー構成を変えたら両サイトに反映されるようになったわけです。どのようにして問題を解決したか、苦労した点なども含めて紹介したいと思います。

同じサイトの誰かにモシモシさせる

まず、解決対象を明確にします。クロスサイトスクリプティングの制限が具体的に起こしている不具合は何かということですが、これは、iframe内から親フレーム(またはウィンドウ)のロケーションを変更できないという問題を示します。メインメニューとフッターメニューの目的はその中に並べられたアンカータグから他のページに遷移することです。iframe内に配置してしまえば、只単にiframe内で遷移することになってしまうので、親のロケーションを変更する必要があります。javascriptから以下のように操作することになります。

parent.location.href = "http://....";
しかし同一ホストではないのでブラウザに無視されて実行することはできません。長らくあきらめていたのですが、@ITで、ブラウザでのクロス・ドメイン通信のセキュリティ保護という記事を見つけました。親が別サイトなら孫を親と同じサイトにしてやりとりをさせれば良いという方法です。なるほど納得です!


IE,Firefox,Safari,Opera,Chromeで確認しましたが全て正しく確認できました。血のつながりのない義理の息子の言うことは聞いてくれないけど孫の言うことは聞いてくれました。孫というか本人同士でしたが。

基本コンセプトを理解して実装していったのですが、二つほど壁がありましたので紹介します。

onclickが効かないよ問題

メニューのアンカーはGWTから動的に作成していましたが、その際に不可解な現象に悩まされました。aタグに入れたonclickイベントが効かないという問題です。iframe化しているので、onclickからbloggerから見て孫となるiframeを動的に生成しなければならないので軽く危機です。静的に書かれているアンカーからは問題ないし、Safari,Chromeでは問題ないし、そもそもonclickイベントに入ってこないのでスクリプトの問題でもないし、大分困惑しましたが、原因は hrefの書き方でした。hrefには "#" だったり、実際のURLを見せて onclick内で return false にしたりすることは、createElementで作成したAタグではやってはいけないということでした。これもセキュリティの問題だったようです。hrefに、"javascript:void(0);" と書くことで解決できました。

iframeにURL渡してもロードされないよ問題

アンカータグからのonclickでは、動的にiframeを作り、「bloggerURL+"#" + 移動したいURL」 を渡さなければならなかったのですが、これまた Safari、Chromeでは移動できたのにIE,Firefoxでは移動しません。やっていたのは以下のようなことです。

Frame frame = new Frame(url);
Document.get().getBody().appendChild(frame.getElement());
FirefoxのFindbugプラグインや、IE8の開発者ツールから見ても、ちゃんとタグが出力されています。
<iframe src="http://..."></iframe>
にもかかわらず、読み込まれないのです。これもしばらく調査して解決に至りました。ロケーションを変更するには、document.location.href = "http://..."; と同じように、iframeのdocument.location.hrefを変更する必要があったのです。さてしかし、GWTではframeオブジェクトからdocumentオブジェクトを参照できるものの、locationメンバがありませんでしたので以下のようにJSNIで対応しました。
Anchor result = new Anchor(menu.getTitle(), url);
if (crossDomainUrl != null) {
  final String encodedUrl = encodeUriComponent(url);
  result.setHref("JavaScript:void(0);");
  result.setTitle(url);
  
  result.addClickHandler(new ClickHandler() {
    
    @Override
    public void onClick(ClickEvent event) {
      String iframeUrl = crossDomainUrl + "#go_" + encodedUrl;
      Frame frame = new Frame(iframeUrl);
      frame.setStyleName("hidden_frame");
      FrameElement frameElement = (FrameElement)frame.getElement().cast();
      frameElement.setAttribute("frameborder", "0");
      Document.get().getBody().appendChild(frame.getElement());
      openDocument(frameElement.getContentDocument(), iframeUrl);
    }
    
    private native void openDocument(Document contentDocument, String iframeUrl) /*-{
      contentDocument.location.href = iframeUrl;
    }-*/;
  });
}
もしかしたらGWTで別の方法が用意されているのに見落としているかもしれませんけどね。

とまあ、いろいろハマったりもしましたが、作成したGWT部品をサイトを越えて流用したいという要件を満たすことはできました。同じような機会が出てきたときにこの記事を思い出してくだされば幸いです。でわでわ。


  • ※1: blogger側に取り込む方法としてはレイアウトのテンプレート編集になります。bloggerのテンプレートは、Google Sitesやはてなダイヤリーなどと比べて自由にHTMLやJavaScriptが書けるようになっています。

XMPPを使ったログの通知

AppEngineで開発している時に開発環境ではなく、実環境上でデバッグしたい事がよくあります。しかし、実環境上では当然ログのtailなど出来るわけはなく、ログを見るためにログページへ行って毎回リロードなんてことをしなければならず、これが非常に苦痛です。そこで巷では見捨てられた感もあるXMPPServiceを使ってログを通知出来る仕組みを作ってみました。利用したロギングライブラリは、sl4jとlogback-classicです。

まず、以下のようなXMPPServiceにログを出力する為のAppenderを作成します。

public class XmppAppender extends AppenderBase<ILoggingEvent> {

    private XMPPService service = XMPPServiceFactory.getXMPPService();

    private String notifyTo;

    public String getNotifyTo() {
        return notifyTo;
    }

    public void setNotifyTo(String notifyTo) {
        if (notifyTo == null || notifyTo.isEmpty()) {
            throw new IllegalArgumentException();
        }
        this.notifyTo = notifyTo;
    }

    @Override
    protected void append(ILoggingEvent eventObject) {
        JID jid = new JID(notifyTo);
        Message message = new MessageBuilder()
            .withMessageType(MessageType.CHAT)
            .withRecipientJids(jid)
            .withBody(getLayout().doLayout(eventObject))
            .build();
        switch (service.sendMessage(message).getStatusMap().get(jid)) {
        case SUCCESS:
            return;
        case INVALID_ID:
            throw new LogbackException("failed to append log event for invalid id:" + jid);
        case OTHER_ERROR:
            throw new LogbackException("failed to append log event for unknown error:" + jid);
        default:
            throw new AssertionError();
        }
    }

}

次に、logback.xmlにappenderの設定を書きます。ここで先程作成したXmppAppenderを設定し、notifyToに通知先にするGoogleAccountを設定します。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="XMPP" class="com.gluegent.tools.appengine.logging.XmppAppender">
   <notifyTo>someoneあっとgmail.com</notifyTo>
   <layout class="ch.qos.logback.classic.PatternLayout">
     <pattern>%-5p %d [%t] %m%n</pattern>
   </layout>
 </appender>
 <root>
   <level value="TRACE" />
   <appender-ref ref="XMPP" />
 </root>
</configuration>

ここまでで、プロジェクトに対する設定は終わりです。

最後に、プロジェクトのデプロイ先からの通知を受けられるようにする為に、GTalkクライアントであっとappspot.comを追加します。以上ですべての設定が終わり、プロジェクトをデプロイすればログがGTalkクライアントに通知されてきます。

これで今までのように毎回画面更新をする必要もなくなり、Eclipseでコンソールを見ながら開発しているような感覚で本番環境上でのデバッグが行えるようになります。ただ一点悲しいのが、XMPPServiceの利用はdefault versionになっていないと出来ない為、同一AppId上では複数の開発者が自分の為だけに利用する事が出来ません。Cron、TaskQueueなどについても言えることなので、そのうちversion別の対応がされて欲しいですね。それでは皆様快適?なログライフを~!

2009年12月2日水曜日

Google Analyticsの解析対象から特定アクセスを除外する

Google Analyticsには特定のIPアドレスやドメインからのトラフィックを除外するといった、フィルタを設定することができます。
ということで、グルージェントからのアクセスを除外するようにしました。
フィルタは Analyticsページ右下のリンク「フィルタマネージャ」より設定できます。
設定済みのフィルタが表示されますが、初回は何もありません。
「+フィルタを追加」から新規作成できます。
フィルタ種類で「カスタムフィルタ」を選択することで、どのような設定が可能か確認することができます。
ここでは、社内からのアクセスを除外するので「既定のフィルタ」で「ドメインからのトラフィックを除外」するようにします。
除外するドメインに"gluegent.com"を指定します。
そして、使用できるプロファイルからpirkar.ashikunep.orgを選択、追加して保存します。
これで完了です。

GAE+GWT+Twitter の検索ガジェットの習作

 twitterのトップページの機能と同じですが、発言の中にURLが含まれていると、その直後にiframe内に展開するというものです。Twitter4jを利用しています。

 また、ガジェット領域はそもそも幅が狭いので、IE以外で長いURLなどが改行されないと激しい横スクロールをしてしまいます。この回避のために発言内のタグではない文字全てにwbrタグを入れるようにしました。また、表示できる幅に応じて吹き出し位置を変えるようにしています。

 ガジェットでGWTを使えるようにするポイントはiframe経由にすることです。GWTは動的に自身のドメイン内のリソースを相対パス指定でダイナミックにロードするので、ドメインをまたいで直接配置することが出来ません。そこさえクリアすればGWTでどんどん作れちゃいます。

09.12.3追記  youtubeのURLをインラインのプレイヤーに置き換えてみました。ガジェット内の検索ボックスに「youtube」で検索してみると確認できますよ。

09.12.4追記  Google VideoのURLをインラインのプレイヤーに置き換えてみました。ガジェット内の検索ボックスに「videoplay」で検索してみると引っかかって確認できますよ。

10.10.7追記

  • 発言日時からtwitterにリンクできるようにしました。これにより、対象ツイートへの返信やリツイートが行えるようになります。
  • Twitter APIの利用をサーバーサイドからクライアントサイドで行うようにしました。twitter4jを辞めてgwtのjsonpを使っています。これにより、レスポンスの高速化と、twitter APIリミットによる不具合が解消されました。リミットとは、同一要求元からのAPIコールが1時間150回までしか行えないというtwitterの制限です。これまではGAE一箇所からの要求であったため、本サイトの閲覧者が多いとすぐにパンクしていました。

Google Analytics入れてみました

入れてみたのはPirka'rサイトです。
http://pirkar.ashikunep.org/
Google Analyticsとは、Googleが提供している無料のアクセス解析です。
いろいろ、有用な情報が得られます。
(個人的に見てて面白い。。。)
http://www.google.com/intl/ja_ALL/analytics/
以下、入れてみた手順を記します。
まずは申し込みです。
上記のAnalyticsサイト右上のリンクから申し込んでください。
Googleアカウントを持っていれば、ログインしてAnalyticsに申し込んでください。
入力するのは以下の内容だけです。
1.対象になるWebの情報
2.連絡先氏名
埋め込むスクリプト(トラッキングコード)が指示されるので、対象のサイトの"</body>"タグの直前に追加します。
Pirka'rでは、↓になっています。
<html>
<head>~</head>
<body>
~中略~
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-11851321-1");
pageTracker._trackPageview();</script>
</body>
</html>
以上で準備は完了です。
データ表示まで最大24時間かかるようなので、気長に待ってみましょう。
Analyticsページでステータスのアイコンが緑のチェックマークになればOKです。

2009年11月30日月曜日

datastore-indexes-auto.xmlはそのままRenameしても動かない

Datastoreでクエリを使う場合、indexを定義したdatastore-indexes.xmlというファイルをWEB-INFの下に配置します。
eclipseのpluginを使ってローカル環境で開発を行っている場合、ローカル環境でクエリを行った際に自動的にWEB-INF/appengine-generated以下に"datastore-indexes-auto.xml"というファイルが作成されます。
以下がそのdatastore-indexes-auto.xmlの例です。
<!-- Indices written at Mon, 30 Nov 2009 05:59:46 UTC -->

<datastore-indexes>

    <!-- Used 1 time in query history -->
    <datastore-index kind="Hoge" ancestor="false" source="auto">
        <property name="hoge" direction="asc"/>
        <property name="fuga" direction="asc"/>
    </datastore-index>

</datastore-indexes>
これをそのまま移動、リネームしてWEB-INF/datastore-indexes.xmlにすれば使える・・・と思っていたのですが、どうやらこれではいけないようです。
<!-- Indices written at Mon, 30 Nov 2009 05:59:46 UTC -->

<datastore-indexes autoGenerate="false">
...
</datastore-indexes>
このように、datastore-indexes要素に"autoGenerate"属性が書かれていないとエラーになります。これはWEB-INF/auto-generated/datastore-indexes-auto.xmlを自動生成させるかどうかを設定する値ですが、WEB-INF/datastore-indexes.xmlでは必須になっています。
せっかく自動生成してくれるのだから、そのまま持ってきても動くように生成してほしいところですが…自動生成されたxmlをそのまま使おうとする際には気をつけましょう。

gwt-dndを使ってGWTでDrag&Drop

GWTはAjaxを使って画面遷移なく多くの操作ができますが、ドラッグ&ドロップの操作は標準ではサポートされていません。しかしgwt-dndというライブラリが開発されており、これを使用することでドラッグ&ドロップを使った多くの操作ができるようになります。弊社Webページの管理画面でもこれを使用しています。
ドラッグ&ドロップでどのようなことができるか、についてはgwt-dndのデモページが参考になると思います。デモのソースコードも載っているので、非常に親切です。

グルージェントWebページの裏側

弊社Webサイトは、Google App Engine(GAE)とGoogle Web Toolkit(GWT)を使って作成しています。
トップページや技術情報ページなどには、このブログの記事を一覧表示して、リンクを貼り付けています。この部分はブログが更新されるたびに変更されるべき動的なコンテンツですので、表示すべき内容を管理画面から変更できるようにしてあります。
ちなみにトップページを読み込んでみると分かると思いますが、ページ全体を先に表示させて、ブログ記事の一覧はAjaxでload後に取得して表示する仕組みになっており、この部分をGWTで作っています。
このブログ記事一覧は、それぞれ表示すべき位置のidに関連付けて、Datastoreで管理しています。それぞれのidに関連付けられるエンティティで、表示すべき記事のListを保持しているのですが、AppEngineの管理コンソールのDataViewerではListの編集を行うことはできないので、独自に管理画面を用意する必要があります。
今回、GWTの練習に、ということでこの管理画面をGWTで作成しました。

あまりきれいな見栄えではありませんが、ダイアログボックスの左側で実際にWebページで表示する記事のリストを表示、右側には候補となるBloggerの記事一覧を表示しています。
何か記事を追加したい場合は右側で該当する記事の「Add」ボタンを押すと、画面遷移なく左側のリストに記事が追加されます。左側のリストはドラッグ&ドロップで上下に並び替えが可能なようにしてあり、簡単にリストの順番を変更することができます。また、一番下の「削除」の部分にドロップすると表示するリストから削除することが出来ます。
また、そもそも手動で変更する必要がなく常に指定したタグに該当する記事を最新順に表示したい、という場合は設定をすることでcronで自動的に記事一覧が更新されるようになる仕組みも作りました。必ずしも最新順に並べたいとも限らない(最新情報ではなく、何らかの技術についての記事だけをまとめて表示したい、という場合もあるかも知れません)ので、自動で最新順に更新するか手動で並べ替えて更新するかは設定で切り替えられるようにしてあります。

2009年11月25日水曜日

GAE/JからGData APIを利用する際の設定でハマる

最近、GAE/Jを触り始めた新人です。
 GoogleにはCalenderやBlogger、Picasaといったサービスがありますが、GAE/J上でそれらのサービスと連携したいケースは多くあるかと思います。
 本家のサンプルコードは充実していますので、Googleのアカウントをお持ちの方は試してみると楽しいのではないでしょうか(なんとGoogle Financeのサンプルまであります。)。
そういうわけで、私もGAE/J上からBloggerの記事を投稿してみようとしたら、ハマってしまい。。。
 情けないことに、プロジェクトフォルダのwar/web-inf配下の appengine-web.xml への追加の
設定を忘れてたからということに気付くまで随分と時間がかかりました。
当然ですが、GoogleのFAQにはちゃんと記載されています。
 こうしたドキュメントを調べつつ読むのも勿論、大事なことですが、手っ取り早く
GAE/Jの開発に取り組まれたい方は、手前味噌ですが弊社の書籍をお勧めします。
 今回の件はP.270に記載されていますし、他にも社内でハマったことは細大漏らさず記述してあります。転ばぬ先の杖、是非、書店で一度、手に取ってみて下さい。
 本Blogの他の”濃い"記事群と併せて、お役に立つかと思います。私も熟読します(^^;

appengine-java-sdk-1.2.8-prereleaseリリース

appengine-java-sdk-1.2.8のプレリリース版がリリースされました。プレリリースなのでEclipsePluginはまだでてないようですね。リリースノートの中から気になった点等を挙げてみようと思います。

JAXBをサポート(JDK6)

JDK6には標準で組み込まれているけれど、JDK5系の場合はJAXBのライブラリを追加する必要があります。

QuotaAPIのサポート

これはpython版では前からあったようですが、Javaにもやっと・・・といった感じですね。詳細はまだ見てませんがこれを使って管理者に警告メールで通知などすることが出来るようになりますね。Quota超えそうな場合は読み取り専用にするとか色々出来そうです。

開発環境用エージェントのバグ修正

1.2.5から1.2.6にあげたほとんどの方がはまったと思われる問題が修正されたようです。この問題はかなり時間を割いて調整しなければならなかったので、嬉しいですね。

開発用サーバでのTaskQueue自動実行サポート

1.2.6ではTaskQueueの実行は開発環境の管理画面から自分で実行しなければいけませんでしたが、本番環境同様に自動で実行されるようになりました。 今までのように手動で動かしたい場合は起動時引数に-Dtask_queue.disable_auto_task_executionを追加すれば可能なようです。

Listプロパティのソート順が仕様通りに

降順にしたときの順番がおかしかったようです。

security-constraintでのリダイレクト時にURLのクエリ文字列が除去されてしまう問題の修正

これは知らなかったですが、ひどいですね。直ってよかったです。

TaskQueueのパラメータに同名のパラメータとして複数の値を設定できなかった問題の修正

これも知らなかったですが、悲しいですね。

開発用サーバの親キーを指定したクエリの動作修正

ソート順が期待通りにはなってなかったようです。

管理コンソールにインデックスの構築状況のページ

複合インデックスを作成した場合などの構築状況を確認しやすくなりました。

Quota超えた際のHTTPステータスコードが変更されました

403(Forbidden)から503(Service Unavailable)に変更されたようです。

ApiProxy.DelegateにmakeAsyncCallメソッドがある(追記)

一部の方の間では最早デファクトとなりつつあるmakeSyncCallを使った実装の話ですが、なんと1.2.8からはmakeAsyncCallがあるようです(@shin1ogawaさん情報ありがとうございます)AppEngineが提供しているサービスに対して非同期で呼び出しが行えるとなると、色々なことができそうですね。MakeSyncCallの詳しいお話は@shin1ogawaさんのこちらのブログが詳しいです

2009年11月24日火曜日

分散トランザクション処理の最適化

前回の「送金のトランザクション処理パターン」では、EntityGroupにまたがるトランザクション処理について簡単に紹介しました。

様々なコメントいただきまして(ありがとうございます)、どうやら「Distributed Transactions on App Engine - Nick's Blog」のやり方が非常に優れているようですので、今回は「送金のトランザクション処理パターン」で紹介した手法に最適化を施して、Nickさんのやり方に近付けてみようと思います。

今回紹介する最適化は、前回のような「狭い範囲のACIDトランザクション」と「べき等性による処理の伝搬」を組み合わせた分散トランザクション一般に適用できそうな手法です。そんなに大層なことはしていませんが、例によってまだ構想段階ですので、また至らない点があればご指摘いただければ幸いです。

おさらい

送金のトランザクション処理パターンでは、次のような図でトランザクションの流れを紹介しました。

このうち、実際の処理の部分だけを抜き出してみると、次のようになります。

上図の四角であらわした部分はApp Engineのトランザクション処理を利用する部分で、矢印であらわした部分はべき等性を利用して処理を伝搬しているところです。ここでのポイントは2点でした。

  • (a) トランザクション処理で、クリティカルな部分を操作 (口座残高を減らしつつ、出金処理情報の作成など)
  • (b) べき等性のある処理で、EntityGroupから他のEntityGroupに情報を伝搬

なお、前回に書き忘れてしまったのですが、ここでの「べき等性(idempotency)」は数学におけるべき等よりも少しだけ条件を増やしています。

  • (a) 1回成功すると、2回目以降は作用を起こさない
  • (b) 何度失敗しても副作用を起こさない

コンピュータの世界では、頻繁に「操作が失敗する」ということが起こりえます。前回のエントリや今回のエントリで使っている「べき等」という用語は、上記のように失敗した場合の作用についても規定しています。上記2つを組み合わせると「無限回繰り返すと1回だけ作用が起こる」という性質をもつようになりますので、送金処理では「入金レコードを1回だけ反映する」などのトランザクション処理を適用できない重要な個所で使っていました。

最適化の流れ

さて、前置きはこれくらいにして前回のトランザクション処理に、実際に最適化を施してみましょう。

依存関係のグラフを作る

最適化を行う際に、最初にやることは「処理の流れの整理」です。 前掲の処理の流れを「依存関係のグラフ」に書きなおします。

矢印は依存先の処理に対して伸びていて、前の処理に依存する項目を青い字で、それに依存される項目を赤い字で書いています。

先ほどの図では4つの箱(トランザクション処理)が直列に並んでいましたが、新しい図では(2.b)と(3)が兄弟関係になりました。(3)は(2.b)に依存しないため、上図のようになっています。

トランザクション処理を結合する

各トランザクション処理の依存関係を洗い出したら、次にそれらのトランザクション処理のうち結合可能なものをまとめていきます。ここで使う規則は同じEntityGroupに対する処理はトランザクションで処理できるというものです。

結合可能なものを洗い出すため、対象のEntityGroupごとに色分けしてみます。

すると、(1, 2.b), (2.a, 3)というペアができました。このうち、(2.a, 3)のほうは依存グラフ上で隣接しているため続けて実行できます。この二つを合成してみましょう。

(2.a, 3)を組み合わせたことで、トランザクション処理の回数が1回減りました。残りの部分は交互に色分けされて直列に並んだので、これ以上トランザクションの合成はできなさそうです。

これだけでも動きますが、もう少しだけ単純化します。 なお、結合可能なトランザクション処理を、常にまとめるかどうかは難しい問題に思えます。あまり長すぎるトランザクション処理を行うと、楽観的並行性制御によって常にその処理が失敗するようになるかもしれません。 並行して変更される頻度や、トランザクション処理の長さとの兼ね合いで、結合するかどうかを決めるのがよさそうです。

べき等性に関する処理を除去する

トランザクションが分断されていたころ、個々のEntityGroupへの値や処理の伝搬はべき等性を利用して実現していました。先ほどの最適化のプロセスで、(2.a, 3)のトランザクションを結合したため、従来の(3)に関するべき等性の保障が冗長になります。

べき等性の担保に利用した部分を2重線で消してみました。追加した個所は赤字で記述してあります。 入金処理エンティティの情報をほとんど利用しなくなったため、入金処理エンティティのかなりのプロパティを削減できます。送金番号のユニーク性を保証するだけになりそうです。

これらを組み合わせると、次のようになります。なお、利用しなくなったプロパティは取り消し線を引いてあります。

上記の手順で、Nickさんの紹介する分散トランザクションとほぼ同じになったと思います。 このように、分散トランザクション処理を考える上で、初期状態で多少無駄があっても最適化を施すことで様々な部分を簡略化できそうです。

ちなみに、スタート時点では次のような感じでした。

まとめ

今回の流れをまとめると、次のような3ステップでした。

  1. 依存関係のグラフを作る(トランザクションを結合しやすくする)
  2. 同じEntityGroupに対する隣接するトランザクションを結合する(色分けするとわかりやすい)
  3. 結合したトランザクション処理から、べき等性の担保に使っていた無駄な個所を除去する

あるていど機械的な手順で、「ACIDトランザクション+べき等性による情報の伝搬」で作られた分散トランザクションを最適化できました。よくよく考えるとこの送金処理に関するパターンはBASEトランザクションの特性を持っているので、そちらの世界の知見を使うのがよさそうです。

ちょっと長くなりすぎたので、一度ここでエントリを終了します。 次では、コメントでいただいた「eventually consistentの整合性でdirty readをどうやって抑制するか」という点について考察してみます。

2009年11月18日水曜日

送金のトランザクション処理パターン

App Engineで現実的な送金処理について考え中です。 ドラフト版なので、怪しい点があればご指摘いただければ幸いです。

コメントで情報いただきました。 Distributed Transactions on App Engineで紹介されてる方法と基本的に同じなので、おそらく問題なく動きそうです。ありがとうございました。

今回はこんな図を使います。

この図の読み方は、矢印の方向にユースケースの一連の処理(またはリクエストの処理)が流れていて、右に行くほど時間が経過しています。そして、矢印がくし刺しにしている四角形は、そのユースケース中で操作するエンティティを表しています。 また、左右の位置が同じ矢印は、基本的には同じ時刻に発生したイベントを表しています。上記の図では、A, B, Cがそれぞれの口座エンティティを同時に操作している感じです。

並行性制御(おさらい)

最初の図のように、それぞれのユーザが別々の口座を操作している場合は問題ないのですが、送金処理のように「自分の口座と別の人の口座を同時に操作する」といった処理には注意が必要です。

上記のようにAがBに送金処理を行っている最中に、Bが自分の口座を変更した場合、Bの口座の情報が壊れてしまう可能性があります。たとえば、Aの送金がなかったことになったり(Aの残高はちゃんと減る)します。

このような問題に対処するには、「並行性制御」という技術を使います。これは、同じエンティティが並行処理で操作されそうになる際に、どちらかを待たせたり、片方の処理を失敗させたりしてエンティティの情報が壊れないように制御します。 下図のようなイメージです。

以上、基本的なおさらいでした。以降では並行性制御を伴うエンティティの操作を「トランザクション処理」と呼ぶことにします。

エンティティグループ

App Engineのトランザクション処理は、エンティティグループ (EntityGroup)というエンティティに対して行われます。このEntityGroupについては公式ドキュメントや「App EngineのEntityGroupを理解しよう - ひがやすおblog」などで詳しく説明されているため、知らない人はまずそちらを読むのがよいと思います。

EntityGroupを絡めたモデリングをする場合、注意しなければならない点は次の3つです。

  1. 各エンティティは複数のEntityGroupに所属することができない
  2. 各エンティティが所属するEntityGroupは、そのエンティティの作成時に決定され、変更できない
  3. EntityGroupに対するトランザクション処理は、常にEntityGroup全体に対して行われる

先ほどの例では、Aの口座とBの口座をトランザクション処理していましたが、App Engineでこのトランザクション処理を実現するにはAとBを同じEntityGroupに所属させるという作業が必要になります。

さて、ここで次の例を考えてみます。

上記は、「CがBの口座に送金する」といった処理です。これもトランザクション処理中に行う必要があるため、BとCは同一のEntityGroupにあらかじめ所属している必要があります。 しかし、すでにBはAと同じEntityGroupに所属しており、「各エンティティは複数のEntityGroupに所属することができない」という規則により、A, B, Cは同一のEntityGroupに所属することになります。図のようにトランザクション処理が必要な個所が重複している場合、それらすべてを含むEntityGroupを一つ用意してやる必要があります。

さらに、口座C, D, E, F, ... と考えていくと、送金処理が必要になる際にすべてのエンティティが同一のEntityGroupに属することになります。 このEntityGroupを動的に変更できれば問題ないのですが、「各エンティティが所属するEntityGroupは、そのエンティティの作成時に決定され、変更できない」という規則によりどうしても巨大なEntityGroupを作らざるを得ない状況になります。

さて、このような巨大なEntityGroupを作成した場合、最後の「EntityGroupに対するトランザクション処理は、常にEntityGroup全体に対して行われる」という規則が非常に厄介なものになります。

上記は全て並行制御されるため、「同時に操作できる口座は常に1つ」という非常に残念なシステムが出来上がります。 せっかくApp Engineというスケールしやすいプラットフォームを利用しているにもかかわらず、口座の操作がボトルネックとなってしまうことが容易に想像できます。 RDBを利用する場合にはトランザクション処理の範囲を動的に(行単位で)決定できるため、このような問題は起こりにくかったと思います。

今回は、こんなケースにApp Engineではどう対処すればいいか、といった点についてちょっと考えてみたいと思います。

補償トランザクション処理の問題点

巨大なEntityGroupを作成するとボトルネックになってしまうため、最初にやらなければならないのはEntityGroupを分割することです。

EntityGroupを分割し、Aの口座とBの口座を別のEntityGroupに分割するケースについて考えてみます。 AからBに5000円送金する場合、疑似コードであらわしてみると次のようなプログラムになります。なお、atomic { ... }というのはトランザクション処理の単位だと考えてください。

atomic { // (a)
  Aの口座から5000円引く
}
atomic { // (b)
  Bの口座に5000円足す
}

多くの場合、上記の処理で正しく動作します。しかし、(b)の処理中にエラーが発生した場合、Aの口座から5000円が無駄に消滅するといった問題が起こってしまいます。 このリスクは「補償トランザクション」という技術を使ってある程度削減することができます。

atomic { // (a)
  Aの口座から5000円引く
}
try {
  atomic { // (b)
    Bの口座に5000円足す
  }
}
catch (...) {
  atomic { // (c)
    Aの口座に5000円足す
  }
  ...
}

上記は補償トランザクション処理を含めたコードで、(b)の失敗時に(a)の処理を取り消すような処理(c)が行われます。

しかし、App Engineではこの処理はあまりうまくありません。 (b)が失敗した後にさらに(c)も失敗する可能性があり、さらにApp Engineのリクエスト30秒ルールによって、(c)が成功する前に処理を打ち切られてしまう可能性があるためです。 つまり、App Engineお金を扱うなどのケースでは、補償トランザクションを適用すべきではありません。

考えたこと

App Engineで送金系の処理が行えないのも寂しいので、少し真面目に考えてみました。 先ほどと同様に「AからBに5000円送金する」という例について解説してみます。

まず、AとBのEntityGroupを分離しておきます。

次に、Aの口座から5000円引きます。このとき同時に、トランザクション処理で「出金処理」というエンティティを同一のEntityGroup上に作成します。これは「AからBに5000円送るつもりだ」ということを表すエンティティで、このエンティティができた瞬間にAの口座からは5000円引いた状態なります。 ここで失敗した場合は送金処理全体が失敗しますので、補償も必要ありません。

出金処理エンティティは、「送金番号(xxx)」「Bに」「5000円」「未処理」という情報を持っています。このうち、「送金番号」はデータストア上でユニークな値である必要があります。

次の処理で、Bの所属するEntityGroup上に「入金処理」というエンティティを作成します。これは先ほどの「出金処理」と対になるエンティティで、「AからBに5000円送られた」ということを表します。この時点ではまだBの口座残高は変更されません。

このとき重要なことは、「同じ送金番号を持つ入金処理エンティティがあれば何もしない」という点です。この規則を導入することによって、「1つの入金処理エンティティに対し、最大1つの出金処理エンティティしか作成できない」ということを保証することができます。 このようにエンティティのユニーク性を保証する場合、「App Engineのユニーク制限を正しく理解しよう - ひがやすおblog

この保証がある限り、図中の(2.a)の処理を何度繰り返してもよいことになります。1度成功したら、それ以降はユニーク制約により常に失敗するため、現在の成功/失敗の状態にかかわらず何度もこの処理を行うことができます。

次に、(2.a)の処理が無事完了したら、出金処理エンティティの「未処理」を「処理済み」に書き換えておきます。ちなみに、(2.a)で既に入金処理エンティティが作成されていた場合でも(2.a)は無事完了したとします。

これをやらなくても動きますが、「この出金を処理する必要があるか?」という情報として有用なので、やっておいたほうがよさそうです。 なお、この(2.a)と(2.b)は連続して繰り返すことができます。 何度も(2.a)を繰り返して、成功したときだけ(2.b)を行うことにしておけば、(2.b)で失敗した場合にも(2.a)からやりなおすことができます。

このように、何度繰り返しても同じ結果になるような処理のことを「べき等(idempotent)な処理」と呼ぶようです。これはApp EngineのTask Queueによる処理にも求められる性質で、逆にこれが守られている処理はTask Queueで処理しやすくなります。 この(2.*)の処理はEntityGroupをまたぐ処理ですので、整合性の保証が必要になります。この個所を「べき等性が保証されたエンティティの移動」という処理にすることで、安全にEntityGroupをまたいだ処理を行うことができます。

最後に、Bの入金処理エンティティをもとに、Bの口座に5000円加算します。このとき、トランザクション処理で入金処理エンティティの「未処理」を「処理済み」に書き換えておきます。これらは同一のEntityGroupになるように頑張ったので、App Engineの普通のトランザクション処理で実現できます。

このとき、かならず入金処理エンティティが「未処理」である場合だけ上記の操作を行うようにする必要があります。これを守らないと1度の送金で複数回の入金が行われてしまい、Bの資産が無駄に増えます。 ちなみにこの処理もべき等性のある処理です。一度入金処理を行ったら「処理済み」扱いになるので、何度(3)の処理を繰り返しても入金は1度だけになります。また、何度失敗しても、(3)の処理を繰り返せばいつかは入金に成功するはずです。

以上で送金処理の一通りの流れは終了です。最終的に、Aの口座からBの口座に5000円を送金することができました。おそらく、(1) ~ (3)のどのタイミングで失敗しても、いずれは送金が成功するはずです。

まとめ

以上が今回考えた「送金のトランザクション処理パターン」です。簡単に説明すると次の3ステップでした。

  1. Aの口座残高を減らし、出金処理情報を作成する (トランザクション処理)
  2. Aの出金処理情報から、Bの入金処理情報を作成する (べき等性のある処理)
  3. Bの口座残高を増やし、入金処理情報を処理済みにする (トランザクション処理+ べき等性のある処理)

ポイントとしては、次の2点です。

  • (a) トランザクション処理で、クリティカルな部分を操作 (口座残高を減らしつつ、出金処理情報の作成など)
  • (b) べき等性のある処理で、EntityGroupから他のEntityGroupに情報を伝搬

今回は、トランザクション処理で「~操作」というエンティティを切り出し、それを再試行可能になるようにべき等性を保証しながら、別のEntityGroupに移し変える作業を行いました。 これは強力なMessage Queueがあれば同様のことを行えますが、App Engineにはそのようなものがないので、ユニーク制約や不可逆な変更とTask Queueを利用して同様の機能を実現させようとしています。 EntityGroupを小さくしたまま、EntityGroup間のデータ移動を安全に行っているため、それなりにスケーラブルで堅牢なシステムになりそうな感じではあります。ただ、SQLで1行で書ける内容にしては非常にめんどうです。

なお、上記処理では「dirty readが発生する」という点に注意が必要です。AからBの送金処理をアトミックに行った場合、その前後でAとBの残高の和は一定額になりますが、「送金してる途中」という状態を挟む関係で残高の和が一定にならないタイミングがあります。 整合性のレベルはEventually Consistentとなりますので、重要なデータが外に漏れないようにうまく設計する必要があります。 また性質上、最初のトランザクション処理以外で制約を含むデータの更新を行うのが困難です。例えば、送金先の口座残高に上限がある場合などは、入金処理エンティティを出金処理エンティティに書き換えて、元の口座に送り返す必要があります(元の口座も上限に達してたりすると大変なことになりますが)。

ちなみに、上記の考え方はApp Engineと別のクラウドサービスを組み合わせるときなどにも利用できそうです。その場合、べき等性のある処理でまたぐのはEntityGroupではなく物理的なサーバになります。

2009年11月16日月曜日

テキスト部分一致検索(接尾辞の列挙)

App Engineでテキスト部分一致検索を紹介してみようと思います。 とりあえず新しい発見があったので、あまり効率的でない「接尾辞の列挙」という方法で実現してみます。 なお、サンプルコードは全てSlim3 Datastoreデータストア低レベルAPIを利用して書かれています。

今回のエントリの内容は開発サーバ(1.2.6)で正しく動かないようです。試してみる方は、デプロイすることをお勧めします。

前方一致検索

App Engineのデータストア上で検索を行う場合、インデックスの「レンジスキャン」という方法で目的のエンティティが探し出されます。 これは、データストア上にソートされたインデックスエントリの開始位置と終了位置を指定して、その範囲全体を抜き出すイメージです。

これをうまく利用すると、テキストの「前方一致検索」が実現できます。 テキストはUnicodeの辞書式順序でソートされていて、"abc"という文字列は:

  • "ab" より大きい
  • "abb" より大きい
  • "abc" と同じ
  • "abd" より小さい
  • "ac" より小さい

という関係になります。 厳密には正しくありませんが、末尾にヌル文字(\u0000)を埋めて、検索対象と同じ文字数にそろえてやるとわかりやすいかもしれません。さらにそれらを文字コードであらわすと、次のようになります。

  • \u0061 \u0062 \u0000 (ab) より大きい
  • \u0061 \u0062 \u0062 (abb) より大きい
  • \u0061 \u0062 \u0063 (abc) と同じ
  • \u0061 \u0062 \u0064 (abd) より小さい
  • \u0061 \u0063 \u0000 (ac) より小さい

この特性を利用すると、「"ab"から始まる文字列」というものを先のレンジスキャンで検出することができます。"ab"から始まる文字列は、インデックス上では「"ab"以上, "ac"未満」という範囲です。これを簡略化して「"ab"以上, "ab\uFFFF"以下」として取り扱っているケースも見かけますが、これは文字列中に\uFFFFが出現しない限りは正しく動作します。

しかし、この方法ではテキストを「前方一致検索」することしかできません。たとえば、「"abcdefg"に"cde"という文字列が含まれているか」といった「部分一致」が必要なケースには適用できません。 今回はそんな「部分一致検索」をApp Engineで行う際の、プリミティブな解決方法について紹介してみようと思います。

リストプロパティ

さて、接尾辞の紹介に入る前にApp Engineのデータストアクエリを活用する上で、(個人的に)重要と思われるリストプロパティについて説明します。

リストプロパティはエンティティの中でjava.util.List型, java.util.Set型など、スカラでない値を持つプロパティのことです。 リストプロパティはデータストアインデックスの作られ方が少しほかのプロパティと異なっていて、個々の要素が展開された状態でインデックスが作成されます

このインデックスに対して検索を行うと、「いずれかの要素が該当する」という種類の検索が実現できます。うまく利用すれば1:Nの関係を表すことができます。 たとえば、データストア上にブログのエントリを保存しておいて、それをタグで検索する場合を考えてみます。

まず、下記のようにSlim3 Datastoreでエンティティの定義をします。このとき、java.util.Settagsというプロパティを定義しているため、リストプロパティになります。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    // エントリに付けられたタグ一覧
    private Set<String> tags;

    // ... 以下アクセサ
}

これに対し、"appengine"というタグが付いたエントリを探してみます。

BlogEntryMeta e = new BlogEntryMeta();
List<BlogEntry> appengines = Datastore.query(e)
    .filter(e.tags.contains("appengine"))
    .asList();

びっくりするくらい簡単でした。 Slim3 Datastoreと低レベルAPIを組み合わせる場合、次のように書けます。

BlogEntryMeta e = new BlogEntryMeta();
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Query query = new Query(e.getKind())
    .addFilter(e.tags.getName(), FilterOperator.EQUAL, "appengine");
Iterable<Entity> iterable = ds.prepare(query).asIterable();

低レベルAPIでは、FilterOperator.EQUALという比較を行います。 これはリストプロパティの各要素が、インデックス上で展開されていて、それに対して同値比較を行う(完全一致検索)というのが実際の処理であるためです。 このあたりを完全に理解するために、一度は低レベルAPIを使ってみるのもいいかもしれません。

誤解していたこと

先ほどのブログエントリの例では、タグ名をテキストの「完全一致」で検索していました。 もし、リストプロパティに対して「前方一致」検索を行う場合、複数の要素がマッチする場合があります。

たとえば、先ほどのBlogEntryの例で、あるエントリのtagsに対して"appengine/py", "appengine/java"という2つのタグが設定されていることを考えてみます。 これに対して「"appengine"から始まるタグを含む」という前方一致検索を行う場合、同一のエントリに対して"appengine"から始まるタグ"appengine/py", "appengine/java"が含まれていますので、このエントリが2重にヒットすることになります。

これをやるとクエリを実行した際に重複するエントリが返ってくると誤解していたのですが、低レベルAPIを使ってもちゃんと重複が除去(distinctのような感じ)されて返ってきました。なので下記のように書けます。

String tag = "appengine";
BlogEntryMeta e = new BlogEntryMeta();
Query query = new Query(e.getKind())
    .addFilter(e.tags.getName(), FilterOperator.GREATER_THAN_OR_EQUAL,
        tag) // 開始位置
    .addFilter(e.tags.getName(), FilterOperator.LESS_THAN_OR_EQUAL,
        tag + '\uFFFF') // 終了位置
    ;

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Iterable<Entity> result =  ds.prepare(query).asIterable();

手元のSlim3 Datastoreのバージョン(slim3-blank-EA1-SNAPSHOT-11142009.zipに同梱のもの)ではCollectionAttributeMetaというクラスの制限で上記のクエリが書けないようです。 もしかすると使っちゃいけないクエリかもしれませんので、もうちょっと調査してみます(App Engine上では正しく動いているように見えますが、少なくとも開発サーバでは変な動きをしました)。

コメントにいただいたように、最新のSlim3 (slim3-EA1-20091123.073844-6.jar) ではStringコレクション型のメタプロパティにstartsWith()メソッドが追加されました。試してみたところ、上記の低レベルAPIで書いてたプログラムは下記のように書きなおせます。簡単ですね。

String tag = "appengine";
BlogEntryMeta e = new BlogEntryMeta();
List<BlogEntry> entries = Datastore.query(e)
    .filter(e.tags.startsWith(tag))
    .asList();
for (BlogEntry entry : entries) {
    // ...
}

接尾辞の列挙

前置きが長くなってしまいましたが、ここまでのポイントは3点です。

  1. App Engineのデータストアで文字列を検索する場合、前方一致検索ができる
  2. リストプロパティを利用すると、「いずれかの要素が該当する」という検索ができる
  3. リストプロパティでも前方一致検索ができる(やや不安)

上記の規則を組み合わせると、「テキストを前方一致検索しかできないなら、前方から1文字ずつ削ったリストを用意してやる」という方法でテキストの部分一致検索を実現できます。 今回の「接尾辞」というのは「文字列の前方を削った残り」のことを表していて、その「接尾辞の列挙」は「前方から1文字ずつ削った全ての文字列をリストアップ」することを表しています。

たとえば、"appengine"という文字列を前方から1文字ずつ削っていくと、次のようになります。

  1. appengine (0文字)
  2. appengine (1文字)
  3. appengine (2文字)
  4. appengine (3文字)
  5. appengine (4文字)
  6. appengine (5文字)
  7. appengine (6文字)
  8. appengine (7文字)
  9. appengine (8文字)
  10. appengine (9文字)

上記の文字列を全てリストプロパティに投入(結果が0文字になるものは除外してもよさそう)し、そのプロパティに対して前方一致検索をかけてやると、元の文字列の部分一致検索を実現できます。 たとえば、"eng"という文字列を前方一致検索すると、appengineにヒットします。

簡単にモデルを定義してみると、だいたい以下のような感じです。

@Model
public class SuffixEnumeration {

    @Attribute(primaryKey = true)
    private Key internalKey;
    private Set<String> array; // 接尾辞の列挙

    public SuffixEnumeration() {}

    public SuffixEnumeration(String text) {
        // 接尾辞配列を作成する
        this.array = new TreeSet<String>();
        for (int i = 0, n = text.length(); i < n; i++) {
            array.add(text.substring(i));
        }
        // キーを作成する
        this.internalKey = internalKeyOf(text);
    }

    // 対象のテキストそのものをキーに含める
    public static Key internalKeyOf(String text) {
        return Datastore.createKey(SuffixEnumeration.class, text);
    }

    // キーから対象のテキストを取り出す
    public static String getTextFrom(Key internalKey) {
        return internalKey.getName();
    }

    // ... 以下アクセサ
}

検索は低レベルAPIで行うため、ちょっと読みにくくなってます。 Slim3 Datastoreを使うとこんな感じで書けます。

SuffixEnumerationMeta e = new SuffixEnumerationMeta();
List<Key> keys = Datastore.query(e)
    .filter(e.array.startsWith(word))
    .asKeyList();
for (Key key : keys) {
    // キーから検索結果を取り出す
    String found = SuffixEnumeration.getTextFrom(key);
    // ...
}

ただし、この方法を利用する場合、検索対象の文字数に対して二乗のオーダーで接尾辞の文字数が必要になります。インデックスが大きくなりすぎる気がしますが、20文字くらいのテキストだったら何とかなりそうな数です。

その他

この他にも、N-gramや形態素の利用など、様々なテキスト検索の技術が存在します。それぞれに利害得失がありますので、App Engineでテキスト検索を行う場合には、適切な技術を選択することが必要になります。

今回の例では、「タグの部分一致検索」など、検索対象の文字列が十分に短く、形態素解析やN-gramでは精度に難があるケースなどに利用できそうです。しかし、AND検索を行うことができない点や、データストアの検索で非常に貴重なinequality filterを利用してしまう点などが悩みどころです。

2009年11月12日木曜日

Slim3 Datastoreに乗り換える(3)

前回Slim3 Datastore の導入方法をエントリにしてみました。今回はそれを利用してエンティティのクラスを作る方法を紹介してみようと思います。と、前回に引っ張ってみたのですが、Slim3 Datastore ではあまりに簡単にエンティティを定義できるのでちょっと趣向を変えようと思います。

今回は、エンティティの定義方法を簡単に紹介した後、普段私が気をつけている点について簡単に紹介してみようと思います。また、経済性を考えて、アプリケーションには次のような制約をつけておきます。

  1. エンティティオブジェクトを永続化層以外でも利用する
  2. エンティティオブジェクトの生成はnewで行う

このあたりはやや宗教的な話も含みますので、参考程度に。

なお、このエントリで言及する注意点などは、少し堅めにApp Engineのデータストアを利用したいひと向けです。非常に小さなアプリケーションを作成する場合や、少数精鋭でライフサイクルの短いアプリケーションを作成する場合などには、少し冗長になると思います。 また、解説の中のパッケージ構成はSlim3推奨のものと異なります。Slim3本体を利用する場合には注意してください。

エンティティクラスの作成

まずはデータストア上のエンティティと1対1に対応するエンティティクラスを作成します。今回サンプルとして取り扱うクラスはブログのエントリ情報を表現するエンティティで、普通に書いた場合は下記のような感じになります。

package com.example.entity;

public class BlogEntry {

    private String permalink;

    private String title;

    public String getPermalink() {
        return this.permalink;
    }

    public void setPermalink(String permalink) {
        this.permalink = permalink;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

このとき、それぞれのプロパティはBean styleのアクセサ(getter/setter)を定義しておいてください。

@Model アノテーションの追加

先ほどのクラスを Slim3 Datastore のエンティティとして利用する場合、クラスに @Model アノテーションをつけてやります。

package com.example.entity;

import org.slim3.datastore.Model;

@Model
public class BlogEntry {
    // ...
}

この状態でセーブすると、「[SILM3GEN1015] You should define @Attribute(primaryKey = true) to one field.」というエラーが表示されます(表示されない場合はSlim3のジェネレータが正しく動いていませんので、注釈プロセッサのファクトリの設定あたりをみなおしてみてください)。このエラーはエンティティにプライマリキーの定義がないために発生しているエラーです。この状態ではエンティティとして利用できませんので、次はプライマリキーを設定してやります。

プライマリキーの追加

それぞれのエンティティにはプライマリキーが必要です。Slim3 Datastore で利用可能なプライマリキーは、App Engine が提供するKey型のプロパティのみです。先ほどのクラスにプライマリキーのフィールドとアクセサ、およびプライマリキーであることを宣言するアノテーションを書いてやります。

package com.example.entity;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Model;

import com.google.appengine.api.datastore.Key;

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    public Key getInternalKey() {
        return this.internalKey;
    }

    public void setInternalKey(Key internalKey) {
        this.internalKey = internalKey;
    }
    // ...
}

Slim3 Datastore のエンティティクラスに必要な最低限の定義は以上です。今回はブログエントリのPermalinkをプライマリキーの名前に利用しようと思いますので、BlogEntry.permalinkフィールドを削除して、BlogEntry.internalKey.getName()で代用する手もあります。ただし、ソートをする場合や前方一致検索をしたい場合などでは、同フィールドは残しておきましょう。

使ってみる

先ほど作成したクラスを実際に使ってみます。Slim3 Datastore を利用してデータストアの操作を行うには、Datastoreという名前のクラスのクラスメソッドを利用します。

まずは、データストアにエンティティを追加するコード片です。前述の通り、BlogEntryエンティティのプライマリキーはPermalinkにすることになっているため、入口のところでPermalinkからKeyオブジェクトを生成しています。

public void put(String permalink, String title) {
    // Permalinkから BlogEntryのキーを作成する
    Key key = Datastore.createKey(BlogEntry.class, permalink);

    // インスタンスを作って値を設定していく
    BlogEntry blogEntry = new BlogEntry();
    blogEntry.setInternalKey(key);
    blogEntry.setPermalink(permalink);
    blogEntry.setTitle(title);

    // Datastore.put() で書きこむ
    Datastore.put(blogEntry);
}

次に、PermalinkをもとにデータストアからBlogEntryエンティティを取得するコード片です。App EngineのデータストアはKey-Valueストア (KVS) のような機能を持っているので、キーさえ分かれば簡単にエンティティを取り出すことができます。

public BlogEntry get(String permalink) {
    // Permalinkから BlogEntryのキーを作成する
    Key key = Datastore.createKey(BlogEntry.class, permalink);

    // 自動生成されたメタデータをインスタンス化する
    BlogEntryMeta meta = new BlogEntryMeta();

    // Datastore.get() で読みだす
    try {
        BlogEntry blogEntry = Datastore.get(meta, key);
        return blogEntry;
    }
    catch (EntityNotFoundRuntimeException e) {
        // 存在しない場合は専用の例外が飛ぶ
        return null;
    }
}

このほかに、Slim3 Datastoreはタイプセーフなクエリを記述できるなど、様々な機能を提供しています。 私見ですが、App Engineのクエリの難しさはクエリの記述自体にはなく、主に問い合わせ先のエンティティの構造に関係しています。 クエリの書き方の紹介は、その辺りのネタと合わせて後日書こうと思います。

以上が Slim3 Datastore のエンティティ入門です。以降は思想やらtipsやらをつらつらと書いていきます。

エンティティクラスに集約させる機能

Slim3 Datastoreを利用する上で地味に問題になるのが、Keyオブジェクトの取り扱いです。前項の例ではブログエントリのPermalinkをプライマリキーとして取り扱って、それをもとにKeyオブジェクトを構築していました。

Keyというのはかなりいろいろな情報を持つオブジェクトで、アプリケーションからは次のような項目が設定できます。

エンティティの種類(カインド)
先ほどの例ではBlogEntryとなる。
キー名
先ほどの例ではPermalinkの文字列を利用していた。(この代わりに自動採番される数値を使うこともできる)
親キー
エンティティグループを構成する場合に必要になる。

App Engineのデータストアを利用する場合、このように複雑な情報を持てるKeyオブジェクトは、エンティティの情報と一貫して管理される必要があります。ここでは、エンティティクラスにこのキーの情報を集約させるために、ふだん私が心がけているプラクティスをいくつか紹介します。

キーファクトリをエンティティクラスに作成する

先ほどのBlogEntryの例では、Slim3が提供するDatastore.createKey()というメソッドを利用して外部からKeyオブジェクトを生成していました。このメソッドは様々なエンティティのキーを生成できる汎用的なメソッドで、個々のエンティティのキーをプライマリキーの設計に従って生成するためのものではありません。

そのため、Datastore.createKey()を利用するプログラムがアプリケーション内に散らばってしまうと、エンティティごとのプライマリキー設計に従わないものが紛れ込む可能性があります。

そこで、Keyオブジェクトを生成するメソッドを、エンティティクラスの中に用意してやります。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    /**
     * 指定のPermalinkを持つエントリに対するキーを生成して返す。
     * @param permalink 対象エントリのPermalink
     * @return 対応するキー
     * @throws IllegalArgumentException 引数に{@code null}が含まれる場合
     */
    public static Key createKey(String permalink) {
        if (permalink == null) {
            throw new IllegalArgumentException("permalink is null");
        }
        return Datastore.createKey(BlogEntry.class, permalink);
    }
    // ...
}

上記はDatastore.createKey()を呼び出すだけの簡単なメソッドですが、重要な点が3つあります。

  1. 自然にドキュメンテーションできる

    上記のファクトリメソッドを見れば、Permalinkをもとにキーを生成することが明らかに分かります。文字列以外の情報をもとに構築する場合や、KVSのテクニックの一つに「2つ以上の値を合成してキーを構成する」というものがありますが、利用者側は各プロパティの値や型、区切り文字などを意識せずにキーを取得できます。

  2. 不正なキーを排除できる

    Datastore.createKey()を呼び出す前に、キーの構成情報を検査できます。今回はnull検査だけやりましたが、必要に応じて不正な文字列を排除したりすることもできます。

  3. キーの構成方法を変更できる

    Datastore.createKey()を直接利用しないことで、キーの構成方法を変えた際の変更が容易になります。ただし、キーの構成方法を変えること自体が非常に大変という問題はあります。

ひと手間必要になりますが、上記のような利点がありますので作成しても損はないかと思います。ただし、Datastore.createKey()を直接呼び出すプログラムがクライアントコードに紛れ込んでいるとひどいことになりそうなので、まだ検討の余地はありそうです。

なお、先ほどの例からBlogEntry.permalinkフィールドを削除する場合や、Key only queryなどを利用する場合には、先ほどの逆の操作、つまりキーから値を取り出すためのメソッドを用意するとよいと思います。重要なことは、「キーの意味をエンティティクラスの中で全て規定する」という点です。

コンストラクタにキーオブジェクトを構築するための情報を渡す

エンティティオブジェクトのライフサイクル中に、プライマリキーの値が変更されることはあまりありません。ということで、オブジェクトの生成時にプライマリキーを渡して、以後変更しないようにする、という運用がよいかもしれません。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    /**
     * フレームワークが利用するコンストラクタ。
     * クライアントコードからは利用しないこと。
     */
    public BlogEntry() {
        // 特に何もしない
    }

    /**
     * インスタンスを生成する。
     * @param permalink エントリのPermalink
     * @param title エントリのタイトル
     * @throws IllegalArgumentException 引数に{@code null}が含まれる場合
     */
    public BlogEntry(String permalink, String title) {
        if (permalink == null) {
            throw new IllegalArgumentException("permalink is null");
        }
        if (title == null) {
            throw new IllegalArgumentException("title is null");
        }
        this.permalink = permalink;
        this.title = title;
        // キーオブジェクト自体を引数に取らないようにして、
        // 先ほど作ったキーファクトリを利用して構築する
        this.internalKey = createKey(permalink);
    }
    // ...
}

ここでのポイントは2点です。

  1. Slim3 Datastore用に引数なしのコンストラクタを用意する

    Slim3 Datastoreのエンティティには引数なしのコンストラクタが公開されている必要があります。クライアントコードから利用されないように、適当にコメントを書いておきます。

  2. Keyオブジェクトを直接渡さない

    クライアントコードからKeyオブジェクトを意識することがないように、コンストラクタの引数にKeyオブジェクトを渡さないようにします。キーオブジェクトを渡されてしまうと、エンティティとキーの整合性が崩れる場合がありますので、あくまで「キーを構築するための情報」という範囲にとどめておくことが重要です。ただし、複雑なトランザクション処理を行うなどの一部のケースでは、キーを直接渡したほうがよい場面もあります。そのため、私自身も原理主義的に守っているわけではありません。

もっとしっかり整合性を保つ場合には、エンティティオブジェクトに対するファクトリを用意してあげるのがよいと思います。ただし、エンティティ構造の変更に関するメンテナンスコストが多少上がり、さらに使い勝手が多少落ちるという弊害が考えられますので、このあたりはトレードオフです。

キーを直接利用する際に注意を促す

ここまでに、Keyオブジェクトをエンティティクラスに隠蔽する手段をいくつか紹介しました。ただ、Slim3 Datastoreを使う以上、エンティティクラスからKeyオブジェクトそのものを隠蔽することはできず、何らかの形でアクセサを提供しなければなりません。

最後のプラクティスとして、このKeyオブジェクトをクライアントコードから利用しようとした際に、何となく気持ち悪い気分を開発者に与える方法について紹介します。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    // ...

    /**
     * フレームワークが利用するメソッド。
     * クライアントコードからは利用しないこと。
     * @return the key
     */
    public Key getInternalKey() {
        return this.internalKey;
    }

    /**
     * フレームワークが利用するメソッド。
     * クライアントコードからは利用しないこと。
     * @param internalKey to set
     */
    public void setInternalKey(Key internalKey) {
        this.internalKey = internalKey;
    }
    
    // ...
}

上記は、元のプログラムに対して少しだけコメントを付け加えたものです。ここでのポイントは2点。

  1. プライマリキーのフィールド名を、何となく使うのが憚られる名前にする

    keyとかidとかにすると、普通のプロパティの一部に見えてしまいます。internalKeyなどにしておくと、フレームワーク内だけで利用されるように取ってくれるかもしれません。ちなみにこの対策は、一部の相手に全く効果がありません。いっそ、org.hamcrest.Matcherのように、ひどい名前のメンバ名にして、ソースコード中に出現すると残念な気分になるようにしてもいいかもしれません。

  2. プライマリキーのアクセサにドキュメンテーションを追加する

    使ってはいけないということを明記しておきます。deprecatedの指定をするという手もありますが、Slim3 Datastore や、一部の永続化ロジックからは利用されるため、あまり適切ではありません。この対策も、一部の相手に全く効果がありません。

上記は、「クライアントコードで誤って使わないように」という対策の意味と、今後のメンテナンス性やポータビリティの観点の2重の意味があります。 普段からKeyオブジェクトを意識しないようにクライアントコードを作成することで、永続化に関する領域と、ユースケースに関する領域の作業をちゃんと切り分けて作成できるようになるという利点があります。

長くなりすぎた+時間切れになったので、ここで一度終わりにします。 次回はDomain Driven DesignのRepositoryパターンを使って、エンティティの操作に関する責務を集約するやり方について紹介しようと思います。 今回のKeyオブジェクト周りの話は、Repositoryパターンのための布石なので、片手落ちなエントリになってしまいました。