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パターンのための布石なので、片手落ちなエントリになってしまいました。

2009年11月10日火曜日

あるフレームワークがGAE対応しているかチェックする方法

あるフレームワークがGAEに対応しているかどうかは、提供側のアナウンスを待ったり、問い合わせたり、動作確認してみるなどがありますが、どれも時間が見積もりにくいですし、人手を介するので、デジタルチックに判断できません。

OSSであれば、わりと簡単に判断ができます。それはソース一式依存ライブラリがあればそれも含めてソースをeclipseプロジェクトに展開して放り込むというやり方です。

これをすると、Google のプラグインが反応して、GAEで未サポートなAPIの利用箇所やアンチパターンを使っている箇所をマークしてくれます。

芸のない単純作業ですが、割と確実性が高いと思います。エラーの量も見渡せるので、(これは当分GAE対応は無いな...)といった予測もできます。少なければ自分で改修してみるというチャレンジやフィードバックをしてみるのも良い経験になるでしょう。

Slim3 Datastoreに乗り換える(2)

前回は思想ばかり書いたので、今回はSlim3 Datastoreの導入手順などを書いてみようかと思います。 Mavenizeされたプロジェクト構成はまだ敷居が高いうえに、エントリがかぶりそうなのでEclipseのGoogle Pluginを使った方法を紹介していきます。

なお、導入手順としてはSlim3本家サイトのほうが楽です。どちらかというと、これまでに作成したプロジェクトをマイグレーションするひと向けになりそうです。

App Engineに対応したプロジェクトの作成

Google Plugin for Eclipse の使用という本家ページを参考しにしてEclipseプロジェクトを作成してください。 ただ、エントリを書いているタイミングではEclipse 3.5に関する情報が書いていなかったため、英語版も参考になると思います。 ちなみに、紙媒体のものでは第3章あたりを参考にするとよいかと思います。

まず最初にやること

作成したプロジェクトのコンテキストメニューから"Properties"を選択し、ダイアログの左メニューから"Builders"を選びます。

ここにEnhancerというアイテムがあるので、チェックを外してしまってください。これはJDOを利用する際にモデルクラスを拡張するプロセスで、ソースコードをコンパイルするときに自動的に実行されます。実際にJDOを使わなくてもこのBuilderがいるだけで余計な処理をするため、早めにつぶしておきます。 同様に、プロジェクト内の"src/META-INF/jdoconfig.xml"も不要になるので、削除してしまってかまいません。

Slim3のダウンロード

現在のSlim3はEarly Accessという扱いで、ビルドされた個々のライブラリとして配布されていないようです。 ということで、Downloads - slim3からSlim3用の空のプロジェクトをダウンロードしてきます。

今見たところ、"slim3-blank-EA1-SNAPSHOT-11022009.zip "というバージョンが最新だったので、それをダウンロードして展開しておきます。

ライブラリの追加

次に、Slim3関連のライブラリを先ほどダウンロードしたアーカイブから、作成したプロジェクトへとコピーしていきます。

App Engineで作成するアプリケーションは、標準のJavaサーバアプリケーションと同じ形態ですので、WEB-INF/libディレクトリの直下にライブラリを配置していきます。 WEB-INFディレクトリ自体はプロジェクトのwarディレクトリの直下にあります。

ダウンロードして展開したSlim3のアーカイブから、Slim3のライブラリである"slim3-blank/war/WEB-INF/lib/slim3-EA1-SNAPSHOT.jar"をコピーし、Eclipseプロジェクトの "war/WEB-INF/lib/"にコピーします。 コピーしたら、そのファイルのコンテキストメニューから"Build Path > Add to Build Path"を選択し、Eclipseから見えるクラスパスにも同ライブラリを追加しておきます。

だいたい上記のようになると思います。

フィルタの登録

コメントにてご指摘をいただいて追加。 また、Slim3には完了していないトランザクションをロールバックするフィルタが付属しているようです。 "org.slim3.datastore.DatastoreFilter"というクラスがそれにあたりますので、下記のようにweb.xmlを編集してフィルタを追加しておくと幸せになれそうです。

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
    <!-- ... -->

    <filter>
        <filter-name>datastoreFilter</filter-name>
        <filter-class>org.slim3.datastore.DatastoreFilter</filter-class>
    </filter>   
    <filter-mapping>
        <filter-name>datastoreFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    <!-- ... -->
</web-app>

Slim3のブランクプロジェクトには他にもいくつかのフィルタが登録されていますので、Slim3 Datastoreだけでなくコントローラ部なども利用する方は、そちらも登録しておいたほうがよさそうです。

ジェネレータライブラリの追加

Slim3 Datastoreは前回紹介した通り、コンパイル時にいくつかのコードを生成します。先ほどコピーしてきたのはSlim3のランタイムライブラリだけですので、次はSlim3のジェネレータに関する設定を行います。

まず、作成したEclipseプロジェクトにlibという名前のディレクトリを作成します。他の名前でもかまいませんが、ここではlibという名前で作成したものとして話を進めます。

次に、ダウンロードして展開したSlim3のアーカイブからジェネレータライブラリの"slim3-blank/lib/slim3-gen-EA1-SNAPSHOT.jar"というファイルを、先ほど作成したEclipseプロジェクトのlibディレクトリにコピーします。

こんな感じになります。

ジェネレータの設定

ジェネレータライブラリをプロジェクト上にコピーしたら、Eclipseプロジェクトがそれを使うようにするための設定を行います。ランタイムライブラリとは違い、ビルドパスに追加しただけでは動作しないので注意が必要です。

プロジェクトのコンテキストメニューから"Properties"を選択し、ダイアログの左メニューから"Java Compiler > Annotation Processing"を選びます。このページでは"Enable project specific settings"にチェックを入れたのち、ジェネレータの出力先である"Generated source directory"の設定を"generated"などに書き換えておきます。これは、標準の出力先が".apt_generated"とドットファイルになっており、Eclipseの通常設定だと表示されないためです。

次に、ダイアログの左メニューから"Java Compiler > Annotation Processing > Factory Paths"を選びます。このページでは先ほど同様に"Enable project specific settings"にチェックを入れたのち、"Add JARs"ボタンでコピーしてきたSlim3のジェネレータライブラリを指定します。

動作確認

以上で設定は終わりですので、次のようなプログラムを書いてみることにします。

package com.example;

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

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

@Model
public class Example {
    
    @Attribute(primaryKey = true)
    private Key key;

    private String message;

    public Key getKey() {
        return this.key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

このプログラムを保存すると、プロジェクトの"generated"ディレクトリに、"com/example/ExampleMeta.java"というファイルが自動作成されます。 これは低レベルAPIのエンティティ表現を、先ほど作成したExampleクラスのオブジェクトと相互に変換するためのクラスで、エンティティのメタデータ的な振る舞いをします。

以上でSlim3の最低限の設定は終わりです。 次回はSlim3 DatastoreでApp Engineのデータストアを利用するサンプルと、Slim3 Datastoreのモデリングで私が気をつけている点などを紹介していく予定です。

2009年11月9日月曜日

Slim3 Datastoreに乗り換える(1)

Slim3 DatastoreはGoogle App Engine for Javaのデータストアを操作するライブラリです。 最近JDOからSlim3 Datastoreに乗り換えつつあるので、背景や使い方などをつらつらと書いていきます。

Slim3 Datastoreの特徴

Slim3 Datastoreはデータストア低レベルAPIの薄いラッパーとして作成されています。他のラッパープロダクト(JDO/JPA)と違いApp Engineのデータストア専用に作られているため、提供される機能が非常に直感的で、さらにかなり高速に動きます。 ざっくり説明すると、以下のような機能を提供しています。

  1. データストア上のデータと自作のモデルオブジェクトを相互に変換する

他にも色々とあった気がしますが、Slim3 Datastoreを利用する最大のメリットは上記の点でしょう。 しかもこの変換層をコンパイル時に自動生成しますので、非常に高速に動作します。

なぜJDOを使わないか

App EngineのデータストアをJavaから利用する場合、本家ドキュメントではJDOを推奨しています。

JDO インターフェースを使用しているアプリケーションは、データベース固有のコードを使用することなく、リレーショナルデータベース、階層型データベース、オブジェクトデータベースなどさまざまな種類のデータベースで作業できます。他のインターフェース標準については、JDO を使用し、アプリケーションを別のストレージ ソリューションへと簡単に移行できます

App Engine での JDO の使用

私もApp Engine for Javaの公開当初からJDOを利用してきましたが、次の理由でSlim3 Datastoreに乗り換えることにしました。

  1. App Engineのデータストアを利用した時点でポータビリティが存在しない

    App Engineのデータストア向けにモデリングした場合、それを他のストレージソリューション上でそのまま流用できると思えませんし、いまいちその需要が見えません。JDOで開発している他のアプリケーションをApp Engine上でそのまま動作させてみようという挑戦をする場合に、コードの一部を流用できるかもしれません。現在の私の認識はそんなレベルです。

  2. アプリケーションサーバとJDOの相性がそれほど良くない

    JDOはエンティティのライフサイクル管理がありますが、分散アプリケーションサーバを利用するApp Engineとあまり相性がよくありません。アプリケーションサーバをまたいでライフサイクル管理するならまだしも、個々のサーバで閉じて管理するため、多くの場合邪魔になります。

  3. データストアとJDOの相性がそれほど良くない

    JDOは仕様としてかなり大きく、App Engineのデータストアを操作するためにはかなりオーバースペックです。そのため、JDOで公開されている機能のほとんどがApp Engine上で利用できません。

  4. 初期化が遅い

    JDOを初めて使うときにはPersistenceManagerFactoryというオブジェクトを初期化しますが、これが残念なくらい遅いです。通常のアプリケーションなら問題にならない程度の速度ですが、頻繁にデプロイ/アンデプロイを繰り返すようなelasticなシステムでは致命的です。

  5. 動作も遅い

    データストア上のエンティティを自作のモデルオブジェクトにマッピングする操作が妙に遅い感じです。それ以外にも、エンティティを前述のライフサイクル管理から切り離す(detach)という余計なコピーが多くの場合に必要になるのも速度低下の要因になりそうです。

過去にハマった経験があるのでネガティブな情報が多いですが、だいたいこんな感じです。 ちなみにJPAも利用できますが、あれはRDBにターゲットしたインターフェースですので、JDOよりもさらに相性が悪い状態です。使ったこともないので割愛します。

なぜ低レベルAPIを使わないか

データストア低レベルAPIはApp EngineのデータストアをJavaから利用する上でのプリミティブとなるAPIで、JDOやJPAなどのラッパーから利用されます。 JDOとは異なり非常に高速に動作し、App Engineのデータストア専用に作られているため、APIのミスマッチも見受けられません。App Engineはリバースエンジニアリングを禁止しているため、この中身について詳細を紹介することはできませんので、インターフェースレベルで簡単に問題点(?)を挙げます。

  1. 型安全性がない

    データストアの読み書きはエンティティという単位で行いますが、これをすべてEntityというクラスで表現しています。エンティティが持つプロパティなどはgetProperty(String), setProperty(String, Object)などの型安全でないメソッドを利用することになります。静的型付けのない言語ならまだしも、JavaでこれをやるとString定数とキャストの嵐になり、メンテナンスが困難になります。

  2. 気がつくと機能が増えてる

    App Engine自体が発展途上のサービスなので、気がつくと低レベルAPIの動作が変化したり機能が増えていたりします。そのまま使うと一定のリスクがあるように思えます。

低レベルAPIはデータストア操作のプリミティブなので、上記のような問題点はある意味で筋違いな指摘です。ですが、それなりに洗練されているAPIですので、上記の問題点を解決すればJDOより使いやすいのではないかと思いました。

Slim3 Datastoreに乗り換えた

さて、前置きが長くなりましたが、そんなこんなで今後はJDOをあきらめてSlim3 Datastoreを使っていくことにしようかと思っています。Slim3 Datastoreは低レベルAPIの薄いラッパーで、低レベルAPIと似通ったインターフェースを提供しています。また、ユーザ定義のモデルクラスをもとに前述のEntityオブジェクトとの相互変換ロジックを自動生成する機能を持っていて、ジェネリクスをうまく取り入れて静的に型安全性を確保しているところがかなり使いやすくなっています。

また、パフォーマンスに影響が出る個所でリフレクションを利用せず、ベタにマッピングするコードを生成しているため、非常に高速に動作します。HotSpotVMの性質を考えると、自分で変換コードを書くよりもパフォーマンスが高い可能性があるくらいです。

今回はJDOを捨てた言い訳をつらつらと書いてみましたが、次回から実際にSlim3 Datastoreを使ってみようと思います。 本家のドキュメントもかなり充実していますので、そちらも参照しながら進めていく予定です。

2009年11月6日金曜日

認証あれこれ

GAEの認証

 GAEのアプリケーションを開発するに当たって、まず最初にどのような認証機能が必要かをあらかじめ整理しておくべきです。提供されている認証機能を把握することが先決で、独自の認証機能を開発するというのは最後の手段にすべきです。認証情報をGAEに任せることでユーザーのパスワードがアプリケーションから漏洩するというリスクを背負う必要がなくなります。ただしGAEの認証情報(UserAccount API)には、アカウントID、ログイン状態、GAE管理者か否かの3つの情報がアプリケーションから把握できるに過ぎません。もしアプリケーションで何らかの付属情報を持たせたい場合にはデータストアを使う必要があるでしょう。

 GAEではデプロイするアプリケーションIDの発行の際に認証方式を選択する必要があります。後からこれを修正することができません。選択する認証方式とは、一般のGoogleアカウントを使うのか、特定のAppsドメインを使うのかの二択になります。大きな意味では一般のGoogleアカウントも、@google.com という最大級のAppsドメインの1つと考えても良いと思います。要するにGAEアプリケーションで扱う認証機構の範囲は1つのドメインに属するユーザー全体ということになります。

 さて、ここから更に登場人物が増えます。Apps向けのサードパーティアプリケーションのインフラとしてGAEを使う場合です。Appsドメインに属するユーザーにはApps管理者か否かというユーザーロールがあります。GAEの管理者とApps管理者は同じではありません。Apps管理者か否かは後述するGoogle Apps Apiが関係します。さらに話をややこしくすると登場人物はまだいます。開発者である貴方です。一見、GAEのアプリケーションの管理者と開発者は同じ権限として混同しやすいのですが、Appsドメイン用アプリケーション開発という立ち位置が登場することにより、開発者とGAEアプリケーションの管理者という立場の違いをもたらします。

 例えば特定のAppsに契約していてAppsアプリケーションにない新たな社内で利用するアプリケーションやシステムを構築したいお客さんがいたとします。インハウスの開発者では足りない場合やGAEアプリケーションの開発が得意なところでないとヤバイという優れた嗅覚をお持ちであれば、例えば弊社などに外注することになるでしょう(^^;。GAEの開発環境はよくできていて、GAEアプリケーションの利用者ドメインに属さない開発者であってもDeveloperとしてinviteされていれば、ローカルサーバーでの開発作業もテストもデプロイ作業も可能です。唯一できないのはGAEでアプリケーションに直接ログインしてのテストとなります(同一ドメインであればこの限りではありません)。うまく本番でのテストと分業できるチーミングもやれないことはないといった感じです。但し現実的にはローカルサーバーとGAEは異なる環境です。QuotaもLimitも無いし完全なシミュレーションにはなりません。ローカルサーバーでは一瞬で終わった作業もGAEではタイムアウトするなんてことは日常茶飯事ですし、現実的には別のドメインのステージング環境のアプリケーションを立てる方が良いでしょう。

 以上の内容をまとめると登場人物と役割は以下のようになります。権限は、番号が小さいほど強いと考えて良いでしょう。

 番号 属性          登録方法         属するドメイン     アプリケーション内の利用可能領域
  ----  ---------------------   ------------------  ----------------   ---------------------------------
 1. 開発者         開発者としてInvite  特定不要           属するドメインがアプリと一緒であれば2.と等価。
                                    そうでなければ4.と等価。
 2. 管理者         開発者としてInvite  アプリのドメイン   auth-constraint/role-name/admin と *の領域。
 3. ユーザー(メンバー)  特になし。     アプリのドメイン   auth-constraint/role-name/* の領域。
 4. ユーザー(非メンバー) 特になし。          特定不要           auth-constraintを記述していない領域。

 この表に対してGoogleアカウント以外の、Appsメンバーがどう関係するか見ていきましょう。AppsユーザーはそのAppsドメイン用として、GAE登録時に設定したアプリケーションにのみログインできます。ドメインが一致する限り3.に位置します。混同しやすいのがApps管理者です。Apps管理者とGAE開発者・管理者は同じではありません。もしApps管理者をGAE開発者・管理者にしたい場合はやはりGAEコンソールからApps管理者のユーザーを開発者として登録する必要があります。

 アプリケーションからはweb.xml(デプロイメント・ディスクリプタ)にsecurity-constraint要素を記述して、アプリケーション内のURLパターン毎に、3種類のアクセス方法を選択できます。

  番号 role-name  動作          利用できるユーザー
 ----  ---------- ---------------------- -------------------------------
 1. なし    なし          非メンバー、メンバー、管理者
 2. *      ログイン処理が入る   メンバー、管理者
 3. admin    ログイン処理が入る   管理者

 URLパターンに / や /* を指定すると、security-constraintに記述の無いその他のURLパターン全てが、 / や /* に指定したauth-constraintの設定と同じになってしまうことに、注意が必要です。また、role-name要素の中身を空にしても非メンバーもアクセス可能な領域になりません。role-nameが空の場合は管理者も含めて誰も許可されない領域という意味になります。非メンバーのアクセスを許容するためにはauth-constraintを記述しないようにします。もし / や /* を、* や admin に指定しないのであれば、非メンバー領域の定義をsecurity-constraintを使って指定する必要もなくなります。

Gooogleアプリケーションの認証

 次にAppsアプリケーションと連携するGoogleアプリケーションをGAEで提供することを考えます。これには Google Data API(以下GData)などの利用が挙げられますが、そちらも認証が必要になります。二重に認証するというのは手続きの多い不恰好なアプリケーションになってしまいますので、GDataを前面に使用するのであれば、GDataの認証をメインにしてGAEのsecurity-constraintは管理者用のシステムメンテナンス画面ぐらいに利用するに留めるほうが良いでしょう。

 ただし、GAEの認証だけでGDataの認証をさせなくても良い方法がApps APIに用意されています。2-legged OAuthという仕組みです。これは、AppsユーザーがAppsアプリケーションにアクセスする手順を、Apps管理者が代行(またはシミュレート)するためのものです。

 2-legged OAuthの使い方は、まずApps管理者だけが知り得ているシークレットコードをGDataのリクエストに付加します。さらにAppsユーザーのIDを使って、要求者パラメータを渡します。こうすることで、あるAppsユーザーがログインしないとアクセスできない機能の結果を第3者であるApps管理者が得られるようになります。シークレットコードはApps管理者であればログインできるAppsコンソールの中で、[高度なツール]-[OAuth アクセスを管理]で参照が可能な、「OAuth コンシューマ シークレット」という文字列データになります。同画面の「Two-legged OAuth を有効にする」にチェックが無いと利用できないので注意してください。

 しかし厄介なことに、GData API for Javaのライブラリでは要求するコマンドによっては要求者IDのパラメータを渡すクチが無い場合があります。GDataのソースコード内でフィードを要求するリクエストオブジェクトにOAuth関連のパラメータを追記するように改造しないとなりません。

com.google.gdata.client.Service
/* patch start */
public Map<string, Map<String, String>> fixedParameters = new HashMap<string, Map<String, String>>();
/* patch end */

public GDataRequest createRequest(GDataRequest.RequestType type,
   URL requestUrl, ContentType inputType) throws IOException,
   ServiceException {

 /* patch start */
 Pattern startPattern = Pattern.compile("^(http:|https:)");
 String urlStr = requestUrl.toString();
 String urlStrRemovedProtocol = startPattern.matcher(urlStr).replaceAll("");
 for (String scope: fixedParameters.keySet()) {
   String scopeRemovedProtocol = startPattern.matcher(scope).replaceAll("");
   if (urlStrRemovedProtocol.startsWith(scopeRemovedProtocol)) {
     Map<String, String> extraParameters = fixedParameters.get(scope);
     if (extraParameters != null && extraParameters.size() > 0) {
       int paramStart = urlStr.indexOf('?');
       if (paramStart == -1) {
         paramStart = urlStr.length();
         urlStr = urlStr + "?";
       }
       for (String name: extraParameters.keySet()) {
         int indexOf = urlStr.indexOf(name+"=", paramStart);
         if (indexOf == -1) {
           String value = URLEncoder.encode(extraParameters.get(name), "UTF-8");
           if (!urlStr.endsWith("?")) {
             urlStr += "&";
           }
           urlStr += name+"="+value;
         }
       }
       requestUrl = new URL(urlStr);
     }
   }
 }
 /* patch end */

 GDataRequest request =
     requestFactory.getRequest(type, requestUrl, inputType);
 setTimeouts(request);
 return request;
}
com.google.gdata.client.GoogleService
public void setOAuthCredentials(OAuthParameters parameters,
   OAuthSigner signer) throws OAuthException {
 GoogleAuthTokenFactory googleAuthTokenFactory = getGoogleAuthTokenFactory();
 googleAuthTokenFactory.setOAuthCredentials(parameters, signer);
 requestFactory.setAuthToken(authTokenFactory.getAuthToken());

 /* patch start */
 if (parameters instanceof GoogleOAuthParameters) {
   GoogleOAuthParameters params = (GoogleOAuthParameters)parameters;
   fixedParameters.put(params.getScope(), params.getExtraParameters());
 }
 /* patch end */
}

 もう1つの認証としては普通にIDパスワードを受け取ってGData APIをコールする、User Credentialという方法がありますが、Googleの標準サービスではないサードパーティアプリケーションにあたるものを開発するわけですので冒頭にも挙げたようにユーザーだけでなくアプリケーションにとってもリスクだと思います。やっぱりお勧めできません。

 2-legged OAuthを使わない方法で、User Credentialも使わずに済むのがAuthSubです。これはAppsユーザーだけでなくGoogleアカウントのユーザーも利用ができますのでGoogleのサービスとのマッシュアップでは最も使われている方式だと思います。ユーザーの認証というよりも、ユーザーがGAEアプリケーションをユーザーアカウントに紐つけるような意味合いになります。AuthSub認証で受け取ったトークン情報をGoogleアプリケーションに渡すことで、Googleアプリケーションでは、ユーザーを認識し適切にGData APIの結果を返します。ただしアプリケーションからはユーザーIDさえ把握できなくなります。しかしトリッキーですがユーザーIDを取得する方法もあります。GData で Calendar APIを使い、カレンダー一覧の先頭のエントリのカレンダーIDを取得することです。先頭はかならず本人のデフォルトカレンダーになるので、カレンダーIDをパースして取得できます。またはデフォルトカレンダーのAuthorsプロパティの先頭のPersonのemailでも良いでしょう。詳しくはカレンダーAPIを参照してください。http://code.google.com/intl/ja/apis/calendar/data/2.0/developers_guide.html

 さてそろそろまとめたいと思います。先に示したようにGAEの認証はある特定のドメインという狭い範囲に絞られてしまいます。しかし、GDataとうまく連携することで1つのGAEアプリケーションでマルチドメイン向けのサービスを構築することも不可能ではなくなります。けっこう大きなシステムも作れそうですし、特定ドメイン間で協調しあえて情報共有もできるものも作れそうです。特に弊社GluegentとSIOS間での仕事の連携は多いのでそんなアプリケーションも出して行きたいところです。最後にGAE管理者ではなくApps管理者向けのURLスコープを設けるために、Apps管理者かどうかを判定する実際に使ったテクニックを紹介して、この長文を終わろうかと思います。これも組み込めば、アプリケーション全体の管理機能と、個々のドメイン毎の管理機能を分けて、Appsドメインの管理者に委譲するなんてことも出来そうです。

 Apps管理者かどうかの判断は、Apps APIをうまく使うことで可能になります。幾つかAPIがありますが、Provisioning APIでは管理者でなくても参照可能なリソースがありますし、Provisioning APIの設定が無効化されている場合は、ドメインの一般ユーザーでも管理者でも同じエラーになります。Reporting APIを実行してみて判断するしか今のところ無いようです。サンプルライブラリが Google Codeで公開されていますので、うまく流用すると良いでしょう。http://code.google.com/p/google-apps-reporting-api-client/downloads/list

 Apps管理者かどうかの判断をするにはユーザーのIDとパスワードを入力してもらうしかありません。サンプルライブラリを使って以下のように使うと良いでしょう。

  private static void reportRun(String adminEmail, String adminPassword,
      String domain, String reportName,
      OutputStream out) throws IOException, ReportException {
  
    ReportRunner runner = new ReportRunner();
    runner.setAdminEmail(adminEmail);
    runner.setAdminPassword(adminPassword);
    runner.setDomain(domain);
    runner.setPage(1);
    runner.setHasUserRequestedSinglePage(true);
    runner.runReport(reportName, (String)null, out);
  }
  
  private boolean isAppsAdmin(OAuth oauth) {
    // Apps Report API を使って確認。
    try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      String reportName = "summary";
      reportRun(oauth.getAdminUser(), oauth.getAdminPassword(),
          oauth.getConsumerKey(), reportName, out);
      String result = new String(out.toByteArray(), "UTF-8");
      LOG.info(reportName + "\n" + result);
    } catch (IOException e) {
      LOG.log(Level.INFO, e.toString());
      return false;
    } catch (ReportException e) {
      LOG.log(Level.INFO, e.getReason());
      return false;
    }
    return true;
  }

2009年11月5日木曜日

Quotaのリセット時間は16時?17時?

Google App Engineでは、CPU使用時間や使用帯域、各サービスAPIの使用量などについて1日あたりの制限が割り当てられており、定められた上限以上のリソースを使用することができません(課金することによってその上限を伸ばすこともできます)。
ただしこの「リソースの使用量」は毎日定期的にリセットされ、「リセットされた時間から、次にリセットされるまでの時間までの使用量」に対して上限が設けられることになります。
(弊社執筆のGoogle App Engine for Java [実践]クラウドシステム構築でも、第2章にて解説をしています。)

ところでこの毎日の"使用量リセット"は、何時になされるのでしょうか?
公式ドキュメントには、
A calendar day is a period of 24 hours beginning at midnight, Pacific Time. App Engine resets all resource measurements at the beginning of each day, except for Stored Data which always represents the amount of datastore storage in use.
と記述されており、いわゆる太平洋時間の午前0時、にリセットされることになります。

調べてみると、太平洋標準時間はUTC-8:00、ただし夏時間の期間はUTC-7:00時間になります。
日本の標準時はUTC+9:00時間なので、太平洋標準時との時差は17時間、つまり日本時間で17時にリセットされることになります。
しかし3月第2日曜日午前2時〜11月第1日曜日午前2時の期間は夏時間となっていて、これまでは毎日、日本時間の16時にリセットされていました。

夏時間の期間が終わった今、リセットされる時刻は17時に変更されているのか…
と思って管理コンソールを観察してみたのですが、どうやら今までどおり16時にリセットされているようです。
ただし、管理コンソールのDashboardなどには"Quotas are reset every 24 hours. Next reset: ** hours"という表記があり、どうやらこの Next reset までの時間は日本時間の17時を指しているようです。
もしかして、16時と17時に2回リセットされるのか?と思ったのですが、17時ではリセットされていませんでした。
いったいどちらが正しいのでしょうか?本当に太平洋標準時の午前0時にリセットされるのなら17時が正しいはずなので、夏時間が終わったのにそれがまだ戻っていない、ということなのでしょうか?
もう数日様子を見てみようと思います。

2009年11月2日月曜日

環境設定で苦戦する日々

ここ数日、苦しんでいたのは主に開発環境、実行環境まわりの設定などでした。
どこをどうすれば動くようになるのか分からず、先輩に何度も訊き直し。。。
GAEの問題なのか、GWTの問題なのか、Mavenの仕組みが分かっていないからなのか、とにかくそのあたりの切り分けもできず、調べたり尋ねたりばっかりでなかなか作業が捗らず、という感じでした。
ようやく自分の環境で動くようになった!と思ったら今度は他の人の環境で動かず、その問題を解決するために四苦八苦。。今日ハマっていたのはここと同じ現象だったようです。同じようにハマっている人が既にいて、こうやって議論の記録が残っていて後から見た人がそれを参考にできる、というのは非常に助かりますね。
それにしてもソースコードを共有していても環境によって動いたり動かなかったり、と悩むことになるのはどうにかしたいものです。何にせよまだまだ勉強が必要です。

DataNucleus Enhancerのトラバース範囲を局所化する

GAE SDKでは、JDO/JPAの実装クラスの生成に DataNucleusを使用しています。Googleが提供するeclipseのプラグインではエンティティクラスのコンパイル時にDataNucleus Enhancerがクラスを拡張して保存します。 しかし、まれにDataNuclesがエラーになることがあります。
java.lang.NullPointerException
at com.google.gdt.eclipse.core.ProcessUtilities.cleanupProcess(ProcessUtilities.java:367)
at com.google.gdt.eclipse.core.ProcessUtilities.launchProcessAndActivateOnError(ProcessUtilities.java:271)
at com.google.appengine.eclipse.core.orm.enhancement.EnhancerJob.runInWorkspace(EnhancerJob.java:82)
at org.eclipse.core.internal.resources.InternalWorkspaceJob.run(InternalWorkspaceJob.java:38)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
大きなプロジェクトほど起こりやすくなるようです。おそらくエンハンスするためのクラスを特定するためのトラバース作業中に何らかの不具合で失敗しているのではないかと考えられます。 これを解決するためには、トラバース対象を局所化することで解決できます。 もっとも簡単なのはソースフォルダを1つ追加して、JPAアノテーションを付加したクラス群を移動して、そのソースフォルダだけDataNucleus Enhancerに見させるようにすることです。 例えばこのソースフォルダを src-entityにしたとします。 次にプロジェクトのプロパティを開き、Google > App Engine > ORM を選択します。 この設定画面には、Enhancerが対象とするフォルダが指定されています。問題が起きたとき、ここには srcフォルダが含まれているはずです。 この画面で、src-entityフォルダを[Add...]ボタンから追加し、srcフォルダを[Remove]ボタンで取り除けば、Enhancerは正常に動作するようになるでしょう。