金曜日, 10月 30, 2009

【Google App Engine】 Keyとカウンタは別々に考えるといいかも このエントリーを含むはてなブックマーク



この記事書いているあいだにこんなニュースを見つけた。これはすごいや。
100万PV/日のmixiモバイルアプリをGoogle App Engineで実装した@gclue_akira氏に直撃インタビュー

さて、本題

不可能と思い込んでいたことができることもある

 何度かイタい目に遭うことで、またどうせ出来ないだろうと思い込んでしまうことを学習性無力感という。長期間にわたって鎖に繋がれたままの犬は、鎖を外してもしばらくは逃げないそうである。GAEはもともと情報が少ないのと、嵌りどころ満載なこともあって、学習性無力感に陥ることがしばしばある。だから、一人悶々とやるより、Blogに晒したり、GAE Nightなどの集まりに出向くなどして、情報共有に努めるのがよろしいかと思う。そこで重要なのは、ありのままの事実を晒すということ。現時点で正しい答えを識っているものは皆無に等しいのだから、誰かに正しい答えを訊くよりかは、自分で実際にやってみて、その結果だけを信じるというスタンスが重要な気がしている。また、思い込みによる間違った情報でも、公開することがきっかけとなり、いろいろと議論が深まって、正しい結論へと導き出せるかもしれない。というわけで、今度のGAE Nightへは、そういうスタンスで乗り込むつもりなので、そこんとこよろしく。(間違った情報を晒すつもりはないが言い訳はさせてね・・)
 ところで、当Blogで公開したもののうち、私自身が「学習性無力感」によって不可能と思っていたことがいくつかある。例えば、ココで紹介した前方一致検索は、Low Level APIの2つのFilterを使って、次のように書ける。このように、1つの項目に2つのFilterを指定することが可能で、Bigtable的には、これはRange Scanと解釈されるようである。

 String value = "コーヒー";
 Query query = new Query("Product");
 query.addFilter("product_name",FilterOperator.GREATER_THAN_OR_EQUAL,value);
 query.addFilter("product_name",FilterOperator.LESS_THAN,value + "\ufffd"); // "\ufffd"はUNICODEの最大値


 前記事では、GREATER_THAN_OR_EQUALの一つしか使っていなかったが、LESS_THANを同時に指定することで、”コーヒー”で始まっていないものを省くことができる。これまでaddFilterは1つだけが指定できると思い込んでいたのだが、内部の動きを知って、なるほどと納得した次第である。

1000件までしか検索できないことはない件

 ココでも述べているが、以前はたしかに1000件しか検索できなかった。正確にいうと、JDOでは1000件以上検索できたが、Low Level APIだと1000件が限界だった。ところが、SDK1.2.5では、FetchOptionsのlimitの値を増やして実行することで、2000件以上検索できることがわかった。しかし、30秒ルールの壁があるため、実際には2000件程度が表示の限界だろうと思われる。私たちのアプリでは、2468件の時点で30秒以上となりエラーとなった。(まあ、鎖から開放された犬が全速力で逃げようとして、30M付近の透明の壁にぶち当たったといった感じかな)
 また、Keyによる検索や更新については制限があり、Quotas_and_Limitsによれば、Batch Getは最大1000件まで可能で、Batch Put/Deleteでは最大500件まで可能である。また、countEntities()で取得できる件数も最大1000件であるが、datastore statistics APIを使うと1000件以上カウントできるとのこと(MLより)

The query restrictions are an artifact of the way App Engine's datastore is constructed, which makes certain operations (e.g. queries and reads) very fast and scalable but does limit the types of queries you can make, though you can typically get around these restrictions by re-thinking your model a bit.

We are working on adding built-in cursor support for easier paging through entities and have just added a datastore statistics API for, among other things, getting the total entity count, even if it exceeds 1,000. More details here:

http://code.google.com/appengine/docs/java/datastore/stats.html

And we also have a data export utility included with the SDK to make it easier for you to back up or even move off of App Engine should you choose to, and we're continuing to look at ways of making App Engine, particularly the datastore component, easier to use.

http://code.google.com/appengine/docs/python/tools/uploadingdata.html#Downloading_Data_from_App_Engine


使用例


 Query query = new Query("__Stat_Kind__");
 query.addFilter("kind_name", Query.FilterOperator.EQUAL, "ここにkind名を指定");
 Entity stat = datastore.prepare(query).asSingleEntity();
 Long count = (Long)stat.getProperty("count");


 また、以下のようにすれば、既存のAPIでもcountを取得できる。
 これは、Keyだけを対象にした検索であり実際の値までは取りにいかない。また、Keyはメモリー上のSSTableに存在するため非常に高速と思われる。(これはこれで非常におもしろいアイデアである)
 でもGoogleの人によると、「実際に計ったわけではないがStaticsの方が速いだろう」とのこと。その理由は、全件読むのではなくKindごと管理している件数を返すだけだから。MLのひがさんのポストより


Low level API:
 DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
 int count = ds.prepare(new Query("your kind").setKeysOnly()).asList(FetchOptions.Builder.withOffset(0)).size();


パフォーマンス

  • 92300件のデータをカウントするのにかかる時間を計測
    • Datastore Statistics : 0.553秒
    • 自前カウンタ(base)をkeyでget : 0.030秒
    • prepare.asList().size() : 17.338秒
    • prepare.countEntities()は結果が1000件

カウンタの実装とボトルネックについて

 アプリが扱うデータは無限個が前提だが、30秒ルールや件数取得の問題があって、実際に無限個のデータを全部扱うのは難しい。なので、私たちは、各々のレコードにシーケンス番号をつけ、Pagingなどの部分的な処理を基本とすることで、これらの制約を回避するように工夫してきた。
 シーケンス番号は、レコードのプロパティに保持しており、また、レコードを生成する都度、カウンタを管理するEntityの値をインクリメントする仕組みである。こうすることで、Pagingの際に、どこからどこまで処理したかを特定できるし、全体の個数もわかるようになる。ただし、カウンタ更新とレコード挿入は1つのトランザクションとして実行しなければならず、これがとても重いのが難点であった。例えば、トランザクション処理させるためには、カウンタのEntityとデータのEntityをEntityGroupとして括らなければならないが、そうすると、挿入の都度、Kind全体がロックされることになるので、大きなボトルネックとなってしまう。元来、Datastore書込は読込の10倍以上遅い。さらにDatastoreのトランザクション処理も遅い。そのうえ、テーブルロックに近い設計をしてしまうのは、ありえないぐらい遅くなる。とはいえ、トランザクション処理を諦めるわけにはいかないので、書込に関しては遅くなるのはしょうがない。(せいぜい、Memcacheを駆使した遅延書込で改善できたらいいなあぐらいは考えてはいたが・・。まあ、無力感に満ちた犬ですな)
 でも、いろいろ実装してみると、実は、レコードにつけるシーケンス番号はいらないかもしれないということがわかってきた。たしかに、ユニークなKeyは必要だが、Paging処理であれば、keyだけあれば十分。また、前方一致検索の例のように、複数件ヒットした場合でも、Keyと「商品名」を連結した新たな項目を作ることで対応できる。
 問題は全体の件数をどう取得するかであるが、これは先ほどの例のようなQueryか、Statistics APIを使えば可能である。Keyについては、KeyRange allocateIds()の自動採番を利用する方法もある。どうしても連番が欲しい場合には、カウンタEntityを作成しても構わないが、レコードのKeyとしては使わず、EntityGroupも構成しない方がいいだろう。そうすれば、カウンタそのものをスケールさせることもできる。(例えば、カウンタEntityを複数もちランダムに選択したものをインクリメントする。そして、すべての合計値をシーケンス番号とする、といった具合に。そんなサンプルがGoogle I/Oで紹介されていた。)
 ちなみに、トランザクション処理をしないとどうなるかというと、レコードの更新失敗が起きたときに、カウンタEnitityだけがインクリメントされてしまい、いわゆる歯抜けの状態が起きてしまう。(ユニークな番号としては使える)これは実際に起こる可能性があるものと考えた方がよいが、補償トランザクションなどでデクリメントするような処理は絶対にやってはいけない。(その時点でカウンタEnitityが更新されている可能性があるため)

Keyに何を入れるべきか

 当初はシーケンス番号をKeyとしていたが、普通のプロパティに入れてもFetchできるので、これをわざわざKeyにする必要はない。また前述したように、そもそもシーケンス番号でなくてもPagingはできる。Keyにはユニークなものを入れさえすればいいのだが、allocateIds()などで作成した機械だけがわかる値を入れても、何かもったいない気がする。なので、KeyFactory createKey()を使って、アプリのKeyを入れるのがよいのではないかと思う。このように、Keyさえ作成できれば、allocateIds()で取得したときのように、Batch PUTも可能である。
 実際に私たちはこのような方法で、以下のような商品マスタ管理用のKindを作成した。ここでは、Shop_code+Product_code+Revisionといったように、アプリでユニークになるKeyを作成している。Revisionは、レコードが更新されるたびにインクリメントされる番号で、楽観的ロックに使っている。また、更新前のレコードは消さないでそのまま保存している。そうすることで履歴管理にもなる。Datastoreの容量は膨大にあるので、こういった大福帳のような感じの設計もありかなと考えている。
ただし、履歴をレコードとして持つ場合、Datastore statisticsのcountだと履歴分も全て件数に含まれてしまうので注意が必要。また論理削除する場合も同様に削除データが件数に含まれる。履歴を持つ、または論理削除する設計の場合で、カウンタ機能が必要な場合は自前のカウンタが必要な気がしている。



日曜日, 10月 18, 2009

【雑記】 そろそろMVCモデルについて一言いっておくか このエントリーを含むはてなブックマーク


 なーんて、MVCを語れるほどの知識はないのだが、琴線に触れてしまったので、私なりに言いたいことを言うことにする。
 本当は、こんな話より先に、先日参加したGAE Nightの話や、Winnyの金子さんが無罪になった話を書きたいのだけど、ココとか、ココとか、ココとか、ココとか、毎日毎日毎日毎日、MVCを語られると、何かいいたくて、もう我慢できなくなってしまった。(これはエンジニアの性なのか!?)
 中島さんのBlogのなかで最も釣られてしまうキーワードは「えせ」。これを使うということは、自分の考えだけが正しくて、他は間違いであるということを暗にいっているようなもの。多くの人はそれに反応してしまうから、感情論になって、あまりよい結論は見い出せなくなってしまっているんじゃなかろうか。中島さんの言っていることは概ね理解できるし、RESTfulな設計などは私の考えと被る部分もあって、ほぼ同意できるのだが、アーキテクチャーの議論でありがちな不毛な議論に発展しそうになっているところがちょっと悲しいところ。せっかくいいこといっているのにもったいない感じである。
 MVCモデルそのものを含めて、現時点では何が正しいかわからないわけで、むやみに「えせ」という言葉を使うべきではないと私は思うのだが・・・。
 ちなみに、私が考えているReflex設計は、ココココでもいっているように、そもそもオブジェクト設計をしないので、エセMVCモデルの筆頭みたいなものなのであるが、中島さんのいっている「核となるのはビジネスロジックを含んだModelの部分。そこをしっかりと実装し、内部構造を隠す粒度の荒いインターフェイスを定義し、外から何をされてもデータの整合性が壊れない様にすることは何よりも大切」という考えは同じである。
 エセMVCモデルでありながら、考えが同じってどういうこと?と思われると思うので、そのあたりを中心に話をまとめてみたい。
 あらかじめ言っておくと、この手の話は、本当はスルーするのが一番である。

混乱させている曖昧な言葉・・Model

 まず、曖昧な言葉の筆頭は、Modelである。Modelはビジネスロジックを指して使うこともあるが、O/Rマッパで作成したデータを扱うオブジェクトを意味することもある。後者の意味で使う人が多いので、ドメインドリブンな人から「けしからん!」とよく怒られる。
 何事も曖昧なまま強引に話を進めると酷い目にあうのが常なので、まずはModelという言葉の意味の定義が重要だ。私が好きなModelの定義は、先の記事にも書いてあるとおり、「モデリングの成果物」というもの。つまり、オブジェクトを設計していくことをモデリングいい、その成果物であるオブジェクト(=データと振る舞いをもつもの)がModelなのである。モデリングはドメイン、つまり問題領域全般にわたるので、おのずとビジネスロジックのすべてがModelに含まれることになる。なので、本来、Modelには2面性があって、ビジネスロジックを指す意味で使われることもあるし、データという意味で使っても構わないものなのである。

 また、MVCモデルはオブジェクト設計を前提にしたものであるが、多くの人はロバストネス分析をやって分析モデル(BCEモデル)を作ることを省略している。これらは脳内でやっている人がほとんどだろう。BCEモデルは、抽象クラスの「操作」と「属性」により、データはEntity、制御はControlといったように整理できる単純明快なモデルなのだが、これを設計モデルのMVCモデルに落とすときに、データであるEntityと制御であるControlの一部がModelになるため、これがモデラーのセンスを必要とする非常に難しい作業となってしまっている。
 その様子は、以下の図がわかりやすい。(参照:UMLモデリングのノウハウ、最後の秘訣)
 このとき、Modelに記述すべきビジネスロジックが少ないと、「エセMVC」とか、「ドメインモデル貧血症」とかいわれるはめになるわけだ。



 また、BCEモデルにおいては、EntityからEntityを呼ぶことはできないという制約があるが、ModelにはControlの一部が入るため、ModelからModelの呼び出しが可能になる。DDDではそれを前提に設計するため、逆にControllerの責務が曖昧になっている印象を受ける。
 SCAなどのコンポーネントアーキテクチャーではもっと進んでいて、各コンポーネントはreferenceと、serviceという口をもち、それぞれを結合できるようになっている。また、それらを内包した大きなコンポーネントを作ることができる。このアーキテクチャーにはControlerは存在しない。敢えて言うならマッシュアップサービスということになるのだろう。



混乱させている曖昧な言葉・・ビジネスロジック

 次に曖昧だと思う言葉はビジネスロジックである。ビジネスロジック=すべての処理ロジック(あるいはプログラムコード)というふうに理解すると、Controlに書くべきFlowや、Viewに書くべきJavaScriptも含まれることになる。これらは、それぞれのレイヤに存在すべきビジネスロジックである。ちなみに、Modelに書くべきビジネスロジックのことを、私はドメインロジックと呼んでいる。もちろん、ドメイン(問題領域)のオブジェクトに対して、「外から何をされてもデータの整合性が壊れない様にすること」の処理は当然記述することになる。
 FlowやJavaScriptのStep数が大きくなることがあっても、設計上は関係ないはずなのだが、ドメインロジック以外の部分が大きくなることを嫌がる傾向にあるようだ。
 MVCモデルでは、ドメインのすべてをModelに含めることが基本なので、すべてのビジネスロジックまでも、Modelの中に記述しなきゃならないのではないか、という発想をしてしまう。こんな窮屈な発想では、RESTfulで疎結合な設計に合わなくなると思うし、FlowやJavaScriptの利点を軽視してしまうことになって、マッシュアップやHTML5のような「いまどき」のアプリにも合わなくなってしまうと思う。

 前述したDDDやSCAで何がいいたかったかというと、ドメインにビジネスロジックを集約させることにこだわるのであれば、DDDやSCAぐらいの思い切った発想で設計すべきであろう。これに躊躇してしまうということは、後述のビジネスロジックが本当は各レイヤで分断されていて、それを良しとしているからではないか。ビジネスロジックをModelに集約すべきといったって、実際に存在する各レイヤのロジックを無視するわけにはいかず、結局は中途半端になってしまうのが現状。このあたりがModel集約化の話に私が素直に同意できない一番の理由である。

 ということで考えたのが、ココにも書いているような、各レイヤにビジネスロジックをもたせるという設計手法。ただし、ビジネスロジックは、揮発性※であるということが条件となる。また、インターフェース設計は重要で、基本的にCRUDの4つだけを定義して、パラメータはentityそのものとするのがミソ。(※ 揮発性とは永続化しないこと)

entity = blogic(entity);




先日作った請求書アプリを例に具体的に説明しよう。まず、各項目と金額はドメインであり、Entityを検索すれば正しく得ることができる。では、金額とともに表示する「¥」や「,」はどのレイヤのロジックで編集すべきだろうか?また、消費税額計算や合計はどうすればいいだろうか。私のおすすめは、View、もしくは、Controllerのレイヤで記述すること。これらは、上記のblogic関数を使って表現できるもので、わざわざModelでやらなくても各レイヤでやればよいと思う。それは、データの整合性はModelが保証していて、揮発性の上記のblogic関数を使う限りにおいては、整合性を壊すことは決してないからだ。
 
 実際にいくつかのプロジェクトでこれを実践してみたのだが、パフォーマンスや生産性の面で非常に有効という結果を得ることが出来た。パフォーマンスは、高速化プロジェクト その2 で述べているとおりである。リモートにあるModelではなく、ブラウザというローカル環境で実行する部分が増えるわけだから当然といえば当然である。生産性の面では、以下のように、それぞれのレイヤで分業できるという利点がある。このおかげで非常に短納期に開発できた。



ドメインサービスとしてのReflex BDB

 Reflexの新3層アーキテクチャーにおけるドメインは、EntityのCRUD操作以外の何ものでもないのだが、登録変更削除においては、「外から何をされてもデータの整合性が壊れない様にすること」がもちろん保証されなければならないし、検索においては、単なるPKによる検索だけではない点は補足すべきだろう。例えば、PK以外のKeyによる検索、大小比較、全文検索、Pagingといった検索などについては、要求仕様に応じて、それなりにドメインロジックを実装しなければならない。
 まだ公表する段階ではないのだが、Private CloudのソリューションであるReflex BDBの次のバージョンにおいて、単なるKey/Valueの検索ではなく、ドメインサービスとして機能するようなものを作っている。内部的にはBDBなのでKey/Valueではあるが、GAEのProperty Indexのような仕組みを追加することで、汎用的なKeyによる検索などができるようにするつもり。ちなみに、これはConsistent Hashと伝染プロトコルを使ったスケールアウトアーキテクチャーとなっており、Dynamoのようにそれぞれのノードを柔軟に追加削除できる。(予定)
 また、各ノードのアプリはOSGiで管理されており、ドメインに応じたユーザアプリの開発と配布が容易にできるようになっている。(かもしれない)
 ここで強調したいのは、ドメインサービスとすることで、RDBやKey/Valueといった下位のデータ層を隠蔽しているという点。重要なのは結果だけとなるので、O/Rマッピングとか諸々のめんどくさい話は全部ドメインサービスのなかに閉じ込めることができる。
 Reflex BDBは、単なるKey/Value Storageからドメインサービスへの進化をめざしていく。



Key/ValueStorageではなく、ドメインサービスを提供する
・ 単なるPKによるGET/PUTだけでなく、様々なKeyによるRESTfulなCRUD APIを提供
・ <>比較、Range、前方一致検索、全文検索、Pagingなど




(※ 2010/12現在、Reflex BDBは、Reflex Tagging ServiceのPrivate版として開発中。BDBではなくCasandraかHBASEになる予定。GAE版はこちら
2011/3、Reflex BDBは、Oracle Berkeley Edition版としてリリース予定。Tagging Serviceはデータ操作(Resource Operator)としてのI/Fが役割となる。)
2012/5 reflexworks で詳細を発表

やっぱりMVCモデルは進化する

 冒頭、この手の話は、本当はスルーするのが一番であるといったが、最後にそれについて補足する。
 私自身、長年にわたってオブジェクト設計やMVCなどを議論してきて、誰もが納得する解を見出すために、何百人月以上浪費してきた。しかし、コレ!というものは結局見つかっていない。(多くの支持を得ているMVCモデルでさえこの有様である)
 私以外にも、モデリングにコダワリをもつオブジェクト厨も多いと思う。誰もが納得する汎用的な完璧な解を見い出すために現在もなおモデリングを永遠と続けている輩もいる。しかし、完璧な正解はないと思うし、ある程度妥協して完成を見い出ださないと、永遠に時間ばかりが過ぎていく。人生なんてすぐに終わってしまうだろう。特に実際のプロジェクトでは納期が重要。設計に関しては、必要に応じて最適なアーキテクチャーを選択しながら柔軟に考えていけばよいと思う。Bugのない品質のよいものを作り上げるのが一番大事で、次に生産性とか、保守運用性とかの話がくるべき。
 Reflex設計が、そもそもオブジェクト設計をしないというのは、モデリングにかかる膨大な工数を削ることが第一の目的である。Entityだけに着目することで、モデラーの感性の入り込む余地を少なくすると同時に、誰でもサクサク設計できるようにしたい。そのための現実的な設計手法なのである。別に「エセMVC」とか、「貧血症」などと呼んでいただいて結構。でもこのおかげで、モデリングにコダワリをもつ輩も「穀潰し」じゃなくなるわけだから、利用する価値があるかもしれないだろう?

 ということで、MVCモデルは進化する、ってことでいいじゃん、と思う次第である。

<関連>
 MVCモデルは進化する
 Reflex Tagging Serviceについて話します

火曜日, 10月 13, 2009

【Google App Engine】 TaskQueueはスケールしない!? このエントリーを含むはてなブックマーク


 TaskQueueによって、何がどう改善されるかについて、Reflex iTextを使って具体的に調べてみたので報告したい。特に、パフォーマンスについて、先日の記事(ココ)と比較しながら説明する。

 やりたいことは単純で、大量のPDFをいかに短時間で生成するかである。前回のテストでは、クライアントから大量のリクエストを投げることでこれを実現しようとしたが、リトライ処理が多発してスケールしなかった。では、TaskQueueを使うとどうなるのだろうか。今回はそこにポイントを絞って調べてみた。

TaskQueue概要

 まず、TaskQueueであるが、これはWebHook型のキューシステムで、Task自体を通常のServletとして書くことができる。Googleのサンプルを見ればわかるように、QueueFactory.getDefaultQueue().add()を使って、URLとRequestParameterを与えるだけで、Worker Servletが呼ばれる仕組みになっている。このシンプルさはとても驚きである。
 ただし、WorkerのURLは、呼び出し元のServletと同じプロジェクトに配置しなければならない。(別プロジェクトに分けて、http://などをつけて実行すると、「java.lang.IllegalArgumentException: url must not contain a 'scheme' part - contains :http」というエラーが出る。)


/**
 * Task Queue worker servlet that offsets a counter by a delta.
 */
public class SimpleCounterWorker extends HttpServlet {
 public void doPost(HttpServletRequest req, HttpServletResponse resp)
  throws IOException {
  Counter.createOrIncrement(
    req.getParameter("name"),
    Integer.parseInt(req.getParameter("delta")));
 }
}



/**
 * Servlet that schedules a counter to be increased.
 */
public class SimpleCounterServlet extends HttpServlet {
 public void doPost(HttpServletRequest req, HttpServletResponse resp)
   throws IOException {

  QueueFactory.getDefaultQueue().add(
    TaskOptions.Builder
     .url("/workers/simplecounter")
     .param("name", "thecounter")
     .param("delta", "1"));

  resp.sendRedirect("/simplecounter.jsp");
 }
}


TaskQueueの制約について

 実装はとても簡単に見えるが、アプリケーション全体をうまく機能させるには、排他制御を考慮しながらMemcacheなどを使ってやりとりしなければならないなど結構大変だったりする。また、キューに追加できるタスクの総数やMemcache自体に格納できるサイズの制限などもあるので注意が必要である。

TaskQueueを使うに当たって関係すると思われる限界値は以下のとおり。

  • キューに追加できるタスクの総数が最大10万件/1日
  • Memcacheに一度に格納できるのは1MB。
  • Memcacheに登録できる総容量は10GB。
  • クライアントがダウンロードできるのは10MB。


 信頼性という意味では、特にMemcacheについては、いろいろ問題があるようである。そもそも、code.google.comに「アプリケーション側ではキャッシュの値が常に利用可能だと仮定すべきではありません。」とあるので、信頼してはいけない。
 したがって、sharding counterのように、同時に複数のTaskが参照更新するものは、消えている場合を想定して、リクエスターにcounterの初期化ロジックを入れることまで考慮しなければならないだろう。しかし、そうすると、同時に初期化するものが複数存在してしまう可能性があり、排他制御が機能しなくなる可能性もある。

<参考記事>
Task QueueはMapReduceの夢を見るか
Memcacheでスピンロックを実装してTask Queue処理結果を集約してみるテスト
Task Queue君とmemcache君、疑って正直すまんかった

TaskQueueを使った処理の流れ

 Reflex iTextを使ったパフォーマンステストでは、TaskQueueを使って以下のような処理を実行させている。

  • クライアントは1タスクあたりの処理ページ数(以下unit)と、総ページ数(以下total)を指定する。
  • dispatcherは、total / unit (余りは繰り上げ) で算出された数だけ、TaskQueueにpdfservice処理を登録する。
  • クライアントにkeyを返却して一旦終了。
  • pdfserviceはPDFを生成し、byte配列をMemcacheに保存する。(非同期)
  • クライアントはkeyをパラメータにして処理を要求する。
  • dispatcherはTaskが完了しているかチェックし、指定されたkeyの処理が全て完了していればMemcacheからPDFを取り出す。PDFマージ処理を行い、クライアントに返却する。



実行結果

今回のアプリにおいては、MemcacheにPUTできる最大のサイズ1Mb以内にするために、1Taskあたり70Pにする必要がある。(つまり、unit=70)また、1回にダウンロードできるサイズは10Mbであり、1PDFにつき700P以内にする必要がある。この処理において起動されるTaskは10本で、全てのタスクが終了するのに1分20秒かかった。

前回のアプリと比較するために、700ページを12リクエスト(合計8400ページ)処理させた場合について計測してみると、unit=70の場合(700/70*12=120タスク)で約15分かかった。

考察

 実行結果をみてもわかるように、前回の結果が約8千ページを4~5分であったのに対し、TaskQueueを使った今回の結果は約15分と、遅くて残念な結果となってしまった。前回のアプリのような大量にリクエストを投げる場合には、リクエスト数に応じてインスタンス数が増えるが、一方のTaskQueueを使った場合は、同じインスタンス内でのみTaskを起動していたのではないかと想像できる。10Taskで1分20秒で、120Taskで15分なので、ほぼリニアに処理時間が増加している計算である。マルチで実行している場合は、120Taskでも1分20秒で完了するはずだ。
 スケールしない(させない?)のは、同一プロジェクト内でしかWorkerをコールできない仕組みと何か関係があるかもしれない。

 しかしTaskQueueを使うといいこともある。大量にリクエストを投げる方法では、8千ページ前後が限界であったが、TaskQueueを使えば、時間はかかるものの、8千ページ以上のPDF生成処理を行うことができる。(上限はMemcacheの2Gbで、MAX約70万ページ)
 レスポンス速度重視であれば、TaskQueueの多用は禁物であるが、非同期処理で、いろいろな用途はあると思われる。 

<関連>
TaskQueueはスケールしない!?2
TaskQueueはスケールしない!?2

<追記>
 ・ 軽いWorkerタスクを別途用意して連続して負荷をかけることでVMは16程度起動することがわかっている。今度はServletContext+UUIDで負荷分散状況を調べてみた
 ・ WdWeaverさんの実験 スケールアウトの真実

水曜日, 9月 30, 2009

【Google App Engine】 Low level APIで前方一致検索およびPagingについて このエントリーを含むはてなブックマーク


 JDOが全然使えないことが明らかになるにつれ、最近ではLow level APIを使う方も多くなってきた。Low level APIは、シンプルで扱いやすく、パフォーマンスにも問題ないことがうけて支持されているのだろう。しかし、機能的には全然足りないので、自分たちで何とか解決しなければならない課題も多い。前方一致検索もその一つである。

Low level APIで前方一致かつPagingが難しい件

 前方一致検索は、JDOではcontent.startsWithを使えばできるようであるが、Low level APIでも、検索項目をソートすることで一応できることはできる。しかしPagingについては、2Page目以降を特定する方法が必要ななため、できないと思っていた。
 例えば、下図のように、”コーヒー”で前方一致検索した場合に、同じ商品名の”コーヒー010”が複数存在する可能性があるため、次ページにおいて、どのレコードから表示すればよいかわからない。この問題は、Low level APIでは、GREATER_THAN_OR_EQUALなどのInequality filterが、1つしか使えないという制約に起因するものである。(追記10/29:LESS_THANも使えることがわかっている。参照=>Keyとカウンタは別々に考えるといいかも
)そこで、「複雑なクエリのためのプロパティを仕込んでおく方法」を参考に、前方一致検索でかつPagingする方法を考えてみた。ただ、さらに安い順でソートしたり、全文検索などはできない。これらについては、TaskQueueを使って対応するしかないと考えている。
 また、ここでは、商品のデータ件数は無限にあるという前提で考えている。小規模なものであれば考える必要はないのかもしれないが、これまで繰り返し述べているように、クラウドの最大のメリットはスケーラビリティだと思っているので、それを阻害するような制約は排除すべきであると思う。



実装方法およびサンプル

具体的には以下のようにする。

1Page目:

キーワード(word=”コーヒー”)で検索する。Low-level Datastore APIのQueryで、商品名プロパティに対して条件をGREATER_THAN_OR_EQUAL指定。Reflex GAEのEntityConverter#convertでCondition指定をし、対象項目が指定されたwordで始まっていなければ結果に含めないようにする。(この処理はIn Memory)

2Page目以降:

キーワード(word=”コーヒー” )、最後の商品名(nextword=“コーヒー010”)、および、最後の連番(number=009)を指定する。
Low-level Datastore APIのQueryで、商品名+連番プロパティに対してnextword + number条件をGREATER_THANで指定。 EntityConverter#convertでCondition指定をし、対象項目が指定されたwordで始まっていなければ結果に含まないようにする。 (この処理はIn Memory)



前方一致でかつPagingを行うサンプル

1ページ目
2ページ目

火曜日, 9月 15, 2009

【雑記】 補償トランザクションの悪夢 このエントリーを含むはてなブックマーク


 数年前、補償トランザクションを駆使したシステムを構築したのだが、うまくいかずに酷い目にあった経験がある。以下の、Aさん宛てに書いたメール内容を読んでもらえれば想像つくと思うが、これは本当に酷いものだった。私はこのとき、補償トランザクションは絶対やるべきではないと心に誓ったのだった。たぶん、スーパーなプログラマーであれば、こんな酷いことにはならないのだろう。でも、普通のプログラマーであれば、十中八九、同様の結果を招くのではないかと感じている。名誉のためにいっておくと、Aさんはかなり優秀なスーパープログラマーの部類に入ると思う。本来、補償トランザクションは、とても難しいものなのだが、Aさんだから大丈夫かなと安心もしていた。ソースレベルでは完璧なロジックに見えて、テストにおいても問題ないような結果が出るのが補償トランザクションの罠。そこまで示されて、「何をそんなに心配されるのか」といわれると否定のしようがない。しかし、その難しさは、実際に運用してみないと本当の意味でわからない。なぜなら、補償トランザクションが問題になってくるのは本番運用以降であるからだ。想定していなかった補正失敗が発生するのを目の当たりにして大騒ぎになるのはよくあることである。補正失敗時のリカバリーは基本的に運用マターであり、そのあたりの運用リスクをよくよく考慮しなければならないのだが、設計段階ではほとんど気づくことができないものなのだ。今から思えば、補償トランザクションの理屈の甘さが見抜けず、安易に採用を判断した私にも責任があると感じている。Aさんにも苦労をかけて申し訳なかった。しかし、頑張ってACIDにさえしておけば、これほどトラブらなかったのではないかとも思う。ACIDであれば、All or Nothing的に管理されるので大きな問題は起こらない。多少、遅くなったり、実装で苦労するかもしれないが、そこはなんとか乗り越えて、ACIDを使うべきと考える次第である。
 ということで、Entityとトランザクション3で述べたように、GAEにおいてもEntityGroupを使って、最低限のACIDトランザクションを使うべきだと思っている。


Aさん

かねてから申しているように、この不具合は致命的なもので、運用チーム、データリカバリーに深刻な影響を及ぼしています。結果、多くの方に迷惑、システムへの不信をいだかれていることになっています。トランザクション処理がきちんとできていないシステムは基幹システムとはいえません。

また、Bさんにも調査していただいておりますが、本来は開発者であるAさんが調査すべきところだと思います。データ修正も、運用チームやBさんではなく、実際にプログラムを作成されたAさんの方でリカバリーすべきでしょう。厳しいことを申し上げているように思いますが、不具合というのはそれほど重いものであるということを認識すべきだと私は思います。

さらにいえば、この不具合は昨年秋のサービスイン当初から続いており、ずっと問題報告がなされていたにもかかわらず今だに改善されていません。3月のXプロジェクトでも再度依頼しましたが、結局、直りませんでした。このプロジェクトは不具合をすべてなくすことが目的で、それなりの予算を組んで実施したプロジェクトでした。その際、私は会社に全部直すことを約束しております。Aさんもその認識はあったはずです。

もう、トラブルは半年以上続いています。当社としては、この件に関して相当なコストをかけてきましたし、今後も発生するのはいかがなものかと頭を悩ましています。

酷な言い方かもしれませんが、もし、ご自身の技術(スキル)的に解決する自信がない、ということでしたら、他の方に協力を仰ぎ、ソースコードを全部見てもらうぐらいのことはやっていただきたいと思います。いずれにせよ、何かのアクションプランを早急に提示ください。もう限界です。


金曜日, 9月 04, 2009

【Google App Engine】 ついにキター! TaskQueueとXMPP このエントリーを含むはてなブックマーク



ついにきたぞ。

App Engine SDK 1.2.5 released for Python and Java, now with XMPP support


Task Queue API for Java

Python developers have been processing tasks offline using App Engine Task Queues since mid-June, but until now the feature was not available in the App Engine for Java SDK. The 1.2.5 SDK now includes support for creating Tasks and Queues in our Java runtime.


今週末は眠れなさそう。

木曜日, 9月 03, 2009

【Google App Engine】 datastore.Entityにおけるプロパティ変換について このエントリーを含むはてなブックマーク


Reflex ore 1.2.2におけるJSON変換

 今回はまず、Reflex Core 1.2.2の紹介からしたい。このバージョンから、JSONにおいてもハイフン、コロン、属性、テキストノードの表現ができるようになった。
 もともとJSONはプロパティ名と値がペアになったシンプルな構造表現しかできないが、XMLからJSONに変換する際には、名前空間や属性、テキストノードといったものをムリクリ表現しなければならない。先日、ココでお伝えしたとおり、実は1.2.1でも属性表現ができるようになっていたのだが、これには一部不具合があったので修正したのと、名前空間やテキストノードなどにも対応したので、1.2.2として公開させていただくことにした。

1. SONでは、要素名の"-"が"__"に変換されるため"type_str-ing"は、"type_str__ing"となります。
2. XMLの名前空間で用いる”:”は、JSONでは"___"に変換されるため、"xml:atom"は、"xml___atom"となります。
3. XMLの属性は、JSONでは"要素名____属性名"プロパティとなるため、"attr1"は、JSONでは"type_str__ing____attr1"プロパティとして表現されます。
4. XMLのテキストノードは、JSONでは、"_______text"プロパティに変換されます。


datastore.Entityのプロパティ変換について

 次にGAEのdatastore.Entityのプロパティ変換の話。
 GAEには、低レベルAPIが用意されているが、これを使うと検索結果はdatastore.Entityクラスのなかのプロパティと値という形で格納される。このままでは不便なので、JDOのようにJavaオブジェクトに変換する必要があるが、GAEのAPIでは変換APIが用意されていないので、私たちは自作のものを利用している次第である。(ReflexGaeのなかのEntityConverter)
 ところが、datastore.Entityのプロパティ変換は以下のような癖があるため、注意しなければならない。Javaオブジェクトとプロパティ変換という意味ではReflexと同じように一筋縄ではいかないようで、ここでもムリクリやっているのがよくわかる。

 1)元のクラスの項目名(JavaのField名)に"$"があると、datastore.Entityのプロパティでは"24"に変換される。
 2)元のクラスの項目名の先頭に"_"があると、datastore.Entityのプロパティでは""に変換される。項目の間の"_"はそのまま。
   o 例)modelの項目名「_$xml$lang」は、datastore.Entityのプロパティ名が「24xml24lang」となる。
    + -> "$"と先頭の"_"について、プロパティ名を変換して読む必要がある。
 3)プロパティ名が重複すると、名前の末尾に「"_"+番号」が付加される。
   o アルファベットの昇順で、番号なし、"_0"、"_1"、・・・という順番のようである。
   o 例)modelクラスに「test」「_test」「__test」という3個の項目があると、Entityクラスのプロパティ名は順に「test_1」「test_0」「test」となる。
     (abc...よりも、"_"の方が先)


 1)および2)に対応したEntityConverterは、ReflexGae 0.9.6に含めているが、3)については未だ対応していないのでご注意願いたい。これは今のところ、modelの項目名に、「test」「_test」のように、頭にアンダースコアがあり、かつ、同じ名前をつけないようなルールで対応するしかない。 

ReflexGae 0.9.6

 * KeyUtils : 親キーのnameを指定できるようにした
 * EntityConverter : Entityのプロパティ名を指定する時、"$"を"24"に、先頭の"_"を""に変換するよう修正


火曜日, 8月 25, 2009

【HTML5】 XMLシリアライゼーションの意味 このエントリーを含むはてなブックマーク


続・XMLなのかHTMLなのかそれが問題だ

 久しぶりにHTML5とXMLのネタ。私は1年前のこの記事において、HTML5で作成されたコンテンツが実質的にWelformedでなければ、Web世界の時計が大きく逆戻りすることになる、と主張した。ところが、あらためて最近のWeb事情、特にHTML5の記事を調査してみると、「むしろwelformedでない方がよい」といった感じで書かれているものが多く見つかるようになってきていて大変驚いた。今回は、HTML5のXML対応について、とりわけXHTML5とは何なのかについて述べてみたい。

“Misunderstanding Markup” 日本語訳

(訳:ぼくはXHTML構文が好きだ。なぜなら、そうやって学んだから。小文字で書くこと、引用符で括ること、brやimgにスラッシュを書くことに慣れているんだ。この書き方は、とても気持ちがいい。オバルチンを飲みながら、テレビでEvil Deadを見るような感じさ。でも、君はそうじゃないかもしれない。まるで叫んでるように見える大文字のタグや、スラッシュなしのimg, br要素、属性の省略表記が好きかもしれない。HTML 5だと、これらはぼくらの好きなように書けるんだ。“pave the cowpaths”っていう原則のおかげで、書く人のおまかせになるんだ。「お気に召すまま、お好きなように、好きなだけ、どうにでも」ってね。)



続・セマンティック・ウェブにXMLはいらない?

 セマンティック・ウェブは、ユニバーサルな情報空間を目指したバーナーズ=リーの考えを受けて、強靱で拡張性があり、適応性のある情報の基盤としてウェブを発展させるための目標の一つ。ウェブの目指すもの:W3Cの目標
 これまで、利用者が、ウェブ上のリソースを最大限に活用できるようなソフトウェア環境を構築するためには、XHTML化とRDFaなどのメタデータを使ったウェブの注釈が重要であるとされてきた。しかし、RDFaに代表される厳格なXMLベースのセマンティック・ウェブのアプローチはなかなか広まらず、最近では前述の記事のような「お気に召すまま」が流行っていることもあって、そのきざしすら感じられなくなってきているのが現状のようである。つまり、「貴族になって楽をしよう」で述べたボヘミアンの完全勝利。それは、MicroformatやHTML5のように、Webが実質的にXML以外の様々な表現や実装技術によって成り立っており、そもそもXMLで厳格にすべてを表現すること自体に無理があるのではないかという見方が主流となってきている背景がある。セマンティック・ウェブの目的だけ考えれば、データを取り出せて意味の疎通ができればよいのだから、必ずしもXMLでWelformedでなくてもよい。HTMLでは、どう処理するのかを規定されることに重きを置き、microformatsはXMLさえ前提にしていない。そう考えていくと、welformedかどうかは、結局のところ、XMLという1つの実装規約の範疇にすぎないという狭い議論に落とし込まれていく。関連記事

XHTML5って何?

 一方で、HTMLをデータとして処理したいという要求が大きいのも事実である。Reflex iTextは、テンプレートとしてHTMLを用いるが、これがXMLでなかったらパース処理に大変な苦労を強いられることになる。今後標準となると思われるHTML5についても同様で、テンプレートをHTML5にバージョンアップする際には、できることならXHTML表現を使いたいところである。
 そこで、XHTML5の明確な定義を調べることにしたのだが、これがとても曖昧で気持ち悪いのだ。その原因は、XHTML5のスキーマがないということ、それから、XHTML1.xと同じ名前空間を使っているということの2つである。こうなってくると、モジュール化して他のモジュールを自由に取り込めないのではないかとか、極端な話、XHTML1.xのボキャブラリ以外使えないのではないかとか不安にもなってくる。
 ちなみに、私と同じような疑問をもっている方もいるので抜粋して紹介したい。去年の記事なんでちょっと古いが、開発者の率直な意見をまとめてみた より。


what-wgが「HTML5はモジュールに分割せずモノリシックな規格を作る」って決めてしまったからなぁ…

<video src="video.ogv" xmlns:cc=" http://creativecommons.org/ns#"
rel="cc:license" resource=" http://creativecommons.org/licenses/by/2.1/jp/"/>
XHTML 5でこういうことができたらいいのになあ。video要素とRDFaを両方使うとオレオレXHTMLになってしまう。せっかくスキーマを取り払ったんだから、モジュール化して他のモジュールも自由に取り込めるような仕組みがいいと思う。

validatorにかけると
Error: Attribute rel not allowed on XHTML element video at this point.
などのエラーを返される。@relや@resourceをvideo要素に書くとHTML 5準拠にはならないんだよね。ブラウザでの表示やRDFの抽出はできるだろうから実際あまり問題はなさそうだけど、そのXHTML文書が準拠する仕様はどこにも存在しないってのは、ブラウザの独自拡張時代を思い出させてあまり好きではない。HTML 6あたりで「やっぱりCSSみたくモジュール化しよう」ってことになり、Multimedia ModuleとかMetainformation Attribute Moduleとかが定義されて、制作者はいろいろなモジュールの中から好きなものを選んで組み合わせて使えるようになったりしないかなあ。


HTML5がモジュール化しない理由とXML名前空間の試み

HTML5はモジュール化しないの?で述べられているように、HTML5はモジュール化しない。それは、HTML5から見れば、XMLは単なるシリアライズ表現の一つと位置づけているように、あくまでHTMLがもつ機能だけに焦点をあてた結果であるといえる。XHTML5はXMLに見えてXMLではない、というのは言いすぎかもしれないが、少なくとも、名前空間とスキーマを充実させてモジュール化し、必要に応じて組み合わせて利用するといった、ユニバーサルな情報空間構想の元に作られたものではないことは明らかである。XMLの生みの親である村田さんもこの構想について以下のように述べられている。しかし、2005年の時点ではまだ名前空間の試みがうまくいっていないことを吐露されてもいる。

お仕着せスキーマと誂えスキーマ

今後の方向は、お仕着せスキーマにちょっと手直しをして、従来は誂えスキーマでしか得られなかったような利点を
得ることだろう。名前空間も、もともとそのための試みで
あった(残念ながらうまく行っていない)。
XHTMLがモジュール化されたもの、
RELAX NGがスキーマのモジュール化を重視しているのも、
NVDLが名前空間による分割検証を提供しているのも、すべて
この方向を目指す試みである。あと10年もすれば、
お仕着せスキーマにちょっと手直しをするという方法が
有効かどうか結論が出るのだろう。


XMLシリアライゼーションの意味

XHTML5の疑問を解決すべく、メーリングリストに聞いてみることにした。
回答の要点は以下のとおり。

1) XHTML5の名前空間はXHTML1.1と同じものを使うがXHTML1.xとは区別される。ただし、両者を判別する方法は今のところない
2) HTML syntaxとXHTML syntaxは、HTML5 DOMのrepresentationであり、同じ要素をもつ

詳細は以下のとおり。
議論がずれることを恐れたので深く突っ込まなかったが、名前空間の意味と運用について、これほど解釈が違う人がいるとは思わなかった。


XHTML 2が打ち切りになったのは皆さんご存知だと思うのですが、以下の記事のなかで、XHTMLをHTMLのXMLシリアライゼーションとみなす、といった説明があります。

「W3CはXHTMLをHTMLのXMLシリアライゼーション(XML形式への変換)と見なしている。HTML
5仕様にXMLシリアライゼーションを含め,引き続きHTML WGで検討していく。」

http://itpro.nikkeibp.co.jp/article/NEWS/20090703/333143/

名前空間がXHTML1.xのままだとHTML5のタグを使えないものがあるので、完全なシリアライゼーションはできないと私は思います。

単純に、HTML5のままで、welformedなものをXHTML5と呼ぶと解釈したとしても、welformedかどうかを示すヘッダーなり何かがないと不都合です。(つまりXML宣言なんですけど)
その場合、名前空間はXHTML1.x以外の何かになると思われるのですが、最新のDraftにも新しい名前空間は見当たりません。

この話は以下にも書いているように、去年からずっと気になっているところなんですが、いまだにモヤモヤしています。

http://www.virtual-tech.net/blog/2008/08/html5-htmlxml.html
http://www.virtual-tech.net/blog/2008/09/html5-xml.html

また、弊社のReflex iTextのテンプレートの文法をHTML5にあわせるかどうか考え中なんですが、このテンプレートはXMLが前提なんでどうしたもんかと。
つまり、HTML5のタグを使いたいのだけど、welformedなHTML5であることを知ることができるのか、あるいは、HTMLとしてパースしないといけないのか、という点がはっきりしないのです。

このあたりで何かわかる方がいれば、ご意見を伺いたいのですが。

よろしくお願いします。


回答1


Y氏)
XML宣言が必要であることと、名前空間がこれまでのXHTMLと別になることの関連、またそれが「完全なシリアライゼーションにならない」にどう繋がるのかがよく分からないのですが、そこは置いといて。

HTML5で言うところの「XHTML」は、「well-formedなHTML5文書を、application/xhtml+xml または
application/xml で送出したもの」になります。
これが text/html で送出されている場合、それはXHTML5ではなく、HTML5になります。
「どう書いたか」ではなく「どう扱うのか」によって、HTMLとXHTMLという区別がつけられるわけです。

> また、弊社のReflex iTextのテンプレートの文法をHTML5にあわせるかどうか考え中なんですが、このテンプレートはXMLが前提なんでどうしたもんかと。
> つまり、HTML5のタグを使いたいのだけど、welformedなHTML5であることを知ることができるのか、あるいは、HTMLとしてパースしないといけ ないのか、という点がはっきりしないのです。

XMLのどの機能を利用しているかによって答えが変わるのですが、たとえば「
」のように空要素のXML的表現は、HTML構文でも利用できるように定義されています。

=====
私)

早速のご回答、ありがとうございます。

>具体的に何が利用できないか、教えていただけますか?
などは、XHTML1.0や1.1では使えないと思います。
使うためには別の名前空間が必要ですよね。

> HTML5で言うところの「XHTML」は、「well-formedなHTML5文書を、application/xhtml+xml または
> application/xml で送出したもの」になります。
> これが text/html で送出されている場合、それはXHTML5ではなく、HTML5になります。
> 「どう書いたか」ではなく「どう扱うのか」によって、HTMLとXHTMLという区別がつけられるわけです。
なるほど。たとえ中身がXMLであっても、「どう扱うのか」ということを、content-typeで区別しましょう、という話ですね。わかります。
では、application/xml
で送り出したXHTML5とは具体的には何になるのでしょうか。welformedなHTML5なのか、XHTML1.1なのか、あるいは、両方が可能なのか。それが知りたいのです。

それにこだわっているのは、要は、XMLパーサしかもっていない当方の実装上の都合だけなんですが、HTML5互換のXMLが扱えるか、もし、application/xmlで送り出すことで、welformedなHTML5を縛れるとすれば、簡単に目的を果たせて嬉しいんです。
=======
S氏)

ちょうど仕様を読み返しているところでしたので、ご質問を受けて、XHTMLに関して言及しているところも軽く読み直してみました。
正しく理解できているかどうかはわかりませんが・・・(^^;
もし間違いがあったら、皆さんご指摘お願いします。

で、ご質問の意図は以下のようなものかと捉えたのですが、いかがでしょうか?

1. ファイル形式がXHTML5か、XHTML1.xかを見分ける手段がないように思えるが、その通りか?
2. 名前空間がXHTML1.xと同じく「http://www.w3.org/1999/xhtml」のままでは、HTML5のボキャブラリを扱えないのではないか?

こういった意図のご質問だと仮定して、僕の意見を述べさせて頂きますと、

1に関してはその通りだと思います。名前空間も同じですし、配信時のContent-Typeも特殊なものではありませんし。

2に関しては、ボキャブラリを扱えるかどうかは実行系に依存するのではないでしょうか。XHTML1.0しか扱えないブラウザは新要素を取り扱えないでしょうし、今後出てくる新しいブラウザはそれらを扱えるようになる、と。

なので、僕はXHTML5というのは、竹嵜さんのおっしゃった

> 単純に、HTML5のままで、welformedなものをXHTML5と呼ぶ

と言うことで良いのかな、と思っています。
(もちろん、文字エンコーディングの指定方法とか、細かい部分は違うにせよ)

ちなみに、仕様(http://www.w3.org/TR/html5/introduction.html#relationship-to-xhtml-1.x)では

"This specification is intended to replace XHTML 1.0 as the normative
definition of the XML serialization of the HTML vocabulary"

意訳:この仕様(HTML5)は、HTMLボキャブラリをXMLシリアライゼーションするための標準定義である、XHTML 1.0の置き換えとして意図されている

と述べられてます。

その上で、
> また、弊社のReflex iTextのテンプレートの文法をHTML5にあわせるかどうか考え中なんですが、このテンプレートはXMLが前提なんでどうしたもんかと。

ちょっと、この部分の問題意識がとらえきれていないので、役に立つ意見になっているかどうかはわかりませんが・・・
何かの参考になれば幸いです。

=====
私)
質問の意図をまとめていただきありがとうございます。まったくそのとおりです。
ご回答内容で、だいぶ見えてきたのですが、新たな疑問もうかんでいます。将来的に、XHTML5の名前空間って何になるのだろう?、と。

>> 単純に、HTML5のままで、welformedなものをXHTML5と呼ぶ
> と言うことで良いのかな、と思っています。

> 2. 名前空間がXHTML1.xと同じく「http://www.w3.org/1999/xhtml」のままでは、HTML5のボキャブラリを扱えないのではないか?
>ボキャブラリを扱えるかどうかは実行系に依存するのではないでしょうか。

ということは、XHTML5は実質的に当面はXHTML1.xと同じ名前空間を使うということになりますね。XMLで新たなボキャブラリを追加する際に、名前空間を変えるべきかどうかは設計者によるといわれていますが、HTML5の要素が加わったXHTMLと、これまでのものとを区別できないのは、やはり不便な感があります。ああ、モヤモヤ。

http://www.ibm.com/developerworks/jp/xml/library/x-namcar/

「最終的には、XHTML 1.0のすべての範囲に1つの名前空間を使用する新たな仕様を発行することで、XHTMLワーキング・グループは軌道修正を行ないました。この教訓から多くを学ぶべきです。命名された物の間に本当に違いがある場合にのみ、XML名前空間を区別すべきでしょう。

残念ながら、物事に白黒をはっきりと付けられることは滅多にありません。新しいバージョンのボキャブラリが、新たな要素を追加するのはよくあることです。前のバージョンから持ち越された要素の意味は変更されていないかも知れないので、名前空間の変更は不適切であると思うかも知れません。しかし仮に、元の同じ名前空間を使用するとしたならば、新たなボキャブラリに追加された要素を元の名前空間内に設置するのも不適切であると思うでしょう。また、新たな要素のみに違う名前空間を使用するのは、とても賢明な選択とは言えません。結局、ボキャブラリで名前空間を改変するかどうかを判断する(自分自身の)判断力が必要です。」


回答2
N氏)

> 名前空間がXHTML1.xのままだとHTML5のタグを使えないものがあるので、完全なシリアライゼーションはできないと私は思います。
> また、弊社のReflex iTextのテンプレートの文法をHTML5にあわせるかどうか

誤解です。

おそらく、XMLの「名前空間」というのは要素のコレクションとその意味を規定するものである、とお考えなのではないでしょうか。

「XML名前空間の簡単な説明」で説明されているのですが、
http://www.kanzaki.com/docs/sw/names.html
実際には、「要素の分類・同定」以上の機能はありません。

そして、XHTML 1.xの http://www.w3.org/1999/xhtml という名前空間は、
「XHTML 1.xが主に使ってきた名前空間」であるだけで、
「XHTML 1.x だけの名前空間」ではありません。
実際、そのURLを開いてみると、いくつか規格がリストアップされています。

で、この中にHTML5が加わることが何を意味するかというと、例えば、
「XHTML1.0のp要素と、HTML5のp要素が、XMLとして同じ意味を持つ」
ということになります。
また、ご指摘のXHTML1.0になくて、HTML5にはあるsection要素等ですと、
名前空間を考えるまでもなく、XHTML1.0にはないので、ここでは問題ありません。

XHTML 2.0の場合は、XHTML 1.0から要素の意味を変更しようとしました。
その場合は同じ名前空間にあると区別ができないので、別の空間を用意することになります。
つまり、XHTML 1.0のp要素と、XHTML 2.0のp要素は別物、ということになります。

XMLだとちょっと考えづらくなってしまいますが、
CのAPIを想像するとわかりやすいのではないでしょうか。

XHTML 1.0にxhtml_foo(int a)という関数がありました。
XHTML 5.0では互換性を保つように作ったので名前はそのままにしました。
XHTML 2.0では引数aをlong型に変えました。
これをそのまま既存のライブラリと強引にリンクさせようとすると落っこちます。
なので、xhtml2_foo(long a)という名前に変えるわけです。

そんなわけなので、
> HTML5のボキャブラリをXHTML1.xに追加するという明確な定義ってありますかね?
「追加」する必要はありません、そのような機能は名前空間は持っていないんですよ。

> 考え中なんですが、このテンプレートはXMLが前提なんでどうしたもんかと。
> つまり、HTML5のタグを使いたいのだけど、welformedなHTML5であることを知ることができるのか、
> あるいは、HTMLとしてパースしないといけないのか、という点がはっきりしないのです。

これはConformance Requestにあるんですが、
http://dev.w3.org/html5/spec/Overview.html#conformance-requirements
XHTML syntaxのみという選択肢があります。
なので「XHTML syntaxしかサポートしない」と言ってしまえばそれでおしまいです。
HTML syntaxの文書はtext/htmlで送られることになっていますから、
これは非サポート、ということですね。
text/html以外は全てXHTML syntaxです。

さて、懸案のXHTML 1.xとの区別ですが、区別する必要はありません。
全てHTML5として処理してください、名前空間が同じというのはそういうことです。
もしもこれで問題が生じるならばそれはHTML5のバグですから、報告なさるのがよろしいかと思います。
=========
私)
ご丁寧なご指摘ありがとうございます。

>> 名前空間がXHTML1.xのままだとHTML5のタグを使えないものがあるので、完全なシリアライゼーションはできないと私は思います。

これはたしかに誤解ですね。XHTML1.x上位互換のXHTML5が定義されていて、名前空間がhttp://www.w3.org/1999/xhtml
であるという考え方をすれば、シリアライゼーションは可能だと思います。ただ、AtomにしてもRSSにしても、大きなバージョン変化があった際には、名前空間も変わるのが一般的だと思うので、下位互換性という理由があるにせよ、名前空間とはそういうものですというのは言いすぎのような気もしています。機能的な議論はあるのですが、実際の運用のところは、先ほどのメールの添付(http://www.ibm.com/developerworks/jp/xml/library/x-namcar/)のように、ケースに応じていろいろなんだと思います。
また、先ほどのメールにも書きましたが、そもそもXHTML5の定義って何なの?という根本的なところが私のなかで曖昧のまま残っていて、ドキュメントのなかにXHTML1.x以上の定義が見つけられなかったので、それ以上も以下もないという発想が元になったのも事実です。
新しいボキャブラリを含むXHTML5を定義したうえで同じ名前空間を使うのか、単なるXHTML1.1なのか、そこが知りたいポイントなのですが、まだしっくりきていません。

>そんなわけなので、
>> HTML5のボキャブラリをXHTML1.xに追加するという明確な定義ってありますかね?
>「追加」する必要はありません、そのような機能は名前空間は持っていないんですよ。

これは、名前空間の機能というより、XHTML5の定義というふうに解釈していただきたいですね。繰り返し恐縮ですが、XHTML5は、HTML5のボキャブラリをXHTML1.xに追加するような定義になっていますかね。(名前空間の話はもう忘れてください。純粋な定義の確認です。)
もしそうだとしたら、ドキュメントの具体的な箇所を教えていただけないでしょうか。白石さんからいただいた参照を見て、私もそうなんだろうなとは思っていますが、念のため。

>ちなみに、仕様(http://www.w3.org/TR/html5/introduction.html#relationship-to-xhtml-1.x)では

>"This specification is intended to replace XHTML 1.0 as the normative
>definition of the XML serialization of the HTML vocabulary"

>意訳:この仕様(HTML5)は、HTMLボキャブラリをXMLシリアライゼーションするための標準定義で>ある、XHTML 1.0の置き換えとして意図されている

>と述べられてます。
=======
> >> 名前空間がXHTML1.xのままだとHTML5のタグを使えないものがあるので、完全なシリアライゼーションはできないと私は思います。
>
> これはたしかに誤解ですね。XHTML1.x上位互換のXHTML5が定義されていて、名前空間がhttp://www.w3.org/1999/xhtml
> であるという考え方をすれば、シリアライゼーションは可能だと思います。

> ただ、AtomにしてもRSSにしても、大きなバージョン変化があった際には、

これらはHTMLとは事情が違うように思います。
RSSは0.9, 0.91, 1.0, 2.0, 0.92 全て互換性がありませんでした。
なので、名前空間が異なるのは当然でしょう。

Atomも上記のRSS達とは違うので名前空間が異なるのは必然です。
また、Atom 0.3 から 1.0 へもdraftから正式版への変更なので、変えるのは当然でしょう。
が、Atomの改定版が出た時にはHTML5と同じ悩みを持つことになるでしょう。
Atomの事情はよく知らないので想像になりますが、Atomは関連規格が多く、
それらの多くがAtomのnamespaceを指定している以上、変えられないのではないかとは想像しています。

> 下位互換性という理由があるにせよ、名前空間とはそういうものですというのは言いすぎのような気もしています。

下位互換性は名前空間を同じにするかどうか考える際の材料の一つになりますが、
実際にXMLの名前空間を見るときには考慮されません。

XMLの名前空間が持っているのは、
* 名前空間が同じ→同じ名前の要素は同じもの
* 名前空間が違う→全ての要素は全くの別物
ということだけだということです。
そして、ここで同じものならばXMLのレイヤーにおいては区別する手段を持たないということです。
(もちろんversion属性を追加してそこでもっと上のレイヤーで区別することは可能ですが)

これをふまえたうえで、名前空間を同じにするか別にするかは仕様策定者に委ねられているわけです。

> 機能的な議論はあるのですが、実際の運用のところは、先ほどのメールの添付
> (http://www.ibm.com/developerworks/jp/xml/library/x-namcar/)
> のように、ケースに応じていろいろなんだと思います。

うーん、ちょっとこの記事、もっといえば特に
> XHTMLワーキング・グループは、それぞれのXHTML DTDに対応する3つの別々の名前空間を
> 使用することを決定しました
のXHTML WGの人々はちょっと神経質すぎるように感じます。

名前空間はXMLのレイヤーにおいて同じ名前の要素を別物扱いさせる、ただそれだけの効果しかありません。
もっと具体的にいえば、XMLパーサが同じ名前の要素なのに別物扱いしてしまう、という意味になります。
XMLの上に構築された多くの仕様では、ここで別物扱いされると上のレイヤーで同一視することはなかなか困難な作業になります。

実際にその仕様が勧告され、その仕様に沿った文書が、過去の当該仕様に沿った文書や他の関連仕様とそれに沿った文書、
その仕様に沿ったアプリケーションや過去の仕様、関連仕様に沿ったアプリケーション、などなど、と協調して動こうという時にどう振舞ってほしいかを考えれば、おのずと答えは見えてくるのではないでしょうか。

Atomの方が例としてシンプルなのでこちらで話しますと、例えばAtom 2.0で名前区間を変えたとすると、既存のアプリケーションや関連仕様は全てバージョンアップしない限りAtom 2.0の文書を無視するわけです。
この壁を乗り越えるにはよっぽど派手な花火がないと難しいように思えます。

> また、先ほどのメールにも書きましたが、そもそもXHTML5の定義って何なの?という根本的なところが私のなかで曖昧のまま残っていて、

HTML5は今までのようにタグの寄り集まった文法ではなく、DOM (Document Object Model) で定義されています。
つまり、ある構造をもった文書を、ブラウザやその他のアプリケーションがどう表示・処理するかと、
そのの構造の文書どうシリアライズ―ありていにいえば文字列にダンプするかを定めているわけです。
で、ダンプの仕方はHTML syntaxとXHTML syntaxがある。
この点において、「HTML5」とは言いつつも、今までのSGMLの子孫であったHTMLやXML一族のXHTMLとは一線を画しています。

> ドキュメントのなかにXHTML1.x以上の定義が見つけられなかったので、
> それ以上も以下もないという発想が元になったのも事実です。

仕様上は以上でも以下でもありませんね。
同じ名前空間の中にいる兄弟と言ったところでしょうか。
で、たまたま(実際はもちろん意図的なんですが) (X)HTML5 は XHTML 1.x の要素集合をおおむね包含しているだけです。

> 新しいボキャブラリを含むXHTML5を定義したうえで同じ名前空間を使うのか、

「新しいボキャブラリ」をここでどのような意味で使っているのかが問題ではありますが、
推測するに、この認識が正解であるように思います。
で、
Q: 別物のはずなのになぜ同じ空間を用いるの?
A: 互換性のため
Q: 互換性のためなら同じ空間を使ってもいいの?
A: よい、だって別にしたからXHTML 2 は死んじゃったんだもの
Q: 同じ空間だと何か問題が起きるんじゃない?
A: 何も問題が起きないように頑張る
と、こうなるわけです。

どうも HTML 5 WG はそれなりに迷った末に同じ空間を用いることを決定したようですが、
現状の膨大なHTML/XHTMLの資産や、HTML5以前に行われた数々の試みがどうなったかを考えると、同じ空間をもちいるか、黙殺されるか、の二択でしかなかったと思います。

> 単なるXHTML1.1なのか、そこが知りたいポイントなのですが、まだしっくりきていません。

XHTML 1.1 は DOCTYPE 等一定の要件を定義していますが、XHTML5 はそれから外れています。
よって、XHTML5 は XHTML 1.1 ではありません。

> >そんなわけなので、
> >> HTML5のボキャブラリをXHTML1.xに追加するという明確な定義ってありますかね?
> >「追加」する必要はありません、そのような機能は名前空間は持っていないんですよ。
>
> これは、名前空間の機能というより、XHTML5の定義というふうに解釈していただきたいですね。繰り返し恐縮ですが、XHTML5は、HTML5のボキャブラリをXHTML1.xに追加するような定義になっていますかね。(名前空間の話はもう忘れてください。純粋な定義の確認です。)

XHTML 1.x と XHTML 5 は別の規格ですね。
XHTML 5 に沿ったアプリケーションが XHTML 1.1 の文書を解釈できるのは「たまたま」です。
もちろん、その「たまたま」を実現するために、名前空間を同じにするだけでなく、
各種の注釈で既存UAの動作を解説したりと、HTML 5 の策定者達が努力していることは言うまでもありません。
(もちろん、XML的には同じ名前空間の同じ要素を扱っているのだから互換性が期待されるわけですが)

=======
1.7 HTML vs XHTML
http://dev.w3.org/html5/spec/Overview.html#html-vs-xhtml
が直接の参照先かと思います.正確にはDOMもrepresentationの一つです.

また,根底部分の知識として,Architecture of the World Wide Webも一読の価値があるかと思います.
http://www.w3.org/TR/webarch/

======
> 1.7 HTML vs XHTML
> http://dev.w3.org/html5/spec/Overview.html#html-vs-xhtml
> が直接の参照先かと思います.

HTML syntax と XHTML syntax が the DOM に対して用意されている、というのはそこですね。

2.2.1 Dependencies
「The DOM is not just an API; the conformance criteria of HTML
implementations are defined, in this specification, in terms of
operations on the DOM.」
http://www.whatwg.org/specs/web-apps/current-work/#dependencies
で、「DOMで定義されている」というのはこの辺の話です。


月曜日, 8月 24, 2009

【Google App Engine】 Entityとトランザクション3 このエントリーを含むはてなブックマーク


 Scaleするかどうか、それが問題だで主張したとおり、クラウドではスケールしなければ意味がない。以下は、GAEで大規模な処理をやって、特にトランザクション処理で悪戦苦闘した際のメモである。本日の発表資料=>ココにも含まれている。

大量データをINSERTできない件について

 先日、Entityとトランザクション2において、Ownedな関連を使ったUpdateサンプルを紹介したわけだが、実はこれ、大量データをINSERTすると急激に遅くなるという問題を含んでいることがわかった。実際にテストしてみたところ、15000件を超えた時点でタイムアウトが頻発、15300件からはとうとう1件も登録できなくなってしまった。

com.google.apphosting.runtime.HardDeadlineExceededError: This request (c1fe232d20adabba) started at 2009/08/11 07:12:31.975 UTC and was still executing at 2009/08/11 07:13:05.802 UTC.


 原因としては、1)インデックス作成における負荷、2)所有関係を作成する負荷、が考えられるが、3)JDOのトランザクション処理自体に問題がある可能性もあるので、その場合はLow Level APIの利用も視野に入れなければならないと考えた。

トランザクション処理の必要性

 トランザクション処理でここまで嵌ってしまうと、ACIDじゃなくて別のよい方法はないものかという考えに傾いていく。最近よくBASEという考え方が紹介されていて、ACIDが大変なんで、BASEでいいんじゃないかと思うこともある。あるいは、エラー忘却型コンピューティングとか、BASEの1種になるかもしれないが、Saga(ロングトランザクション)モデルといった補正トランザクションなどを利用しようという考え方もある。Sagaは、Webサービスが密に結合しているようなシステムにおいて過去多く見られた。最近のRESTfulなシステムへの導入では、ステートレスになじまないことでやや否定的だが、やろうと思えば可能だろう。(参考:RESTとトランザクション?
 しかし、ロングトランザクションなどのACIDではない仕組みに関しては、補正失敗のケースも考慮しておかないと運用面で支障をきたすことになるので要注意である。
 私は以前作ったWebサービス連携のエラーリカバリーで酷い目にあった経験があるので、この手のものをACIDで組まないことに、ものすごく悲観的である。ACIDによる一貫性保証がないカウンタの実装などは特に嵌る。そして、すぐに不具合を見つけることができないことが問題を大きくする。単体テストでは成功して、本番運用に入っても数日は成功するので一見うまくいくように思うかもしれないが、失敗の失敗、つまり、リカバリーの失敗までを考慮していないと、破綻は必ず訪れることになる。(マーフィーの法則「起きる可能性のあるものは必ず起きる」)(関連:信頼性とジレンマ

調査結果および対応策

 話は戻り、パフォーマンス劣化の原因について調べてみた。そして、なんとかACIDでやる方法はないものか、もう一度考えることにした。

一度に100件登録した場合

<所有関係の負荷>
  * 所有関係あり: 15秒くらいから。件数が増えるとだんだん遅くなる。
  * 非所有: 3秒くらい。件数が増えても変わらない。

<Index作成の負荷>
  * インデックスなしと、インデックスありの、どちらも100件登録で3秒くらい。
  * インデックスありのテーブルで、100000件弱登録できた。
  * 登録にかかる時間も100件約3秒と変わりなし。


 以上の結果のとおり、Indexの数というより、所有関係の方がパフォーマンスへの影響が大きいということがわかった。所有関係については、それを解消しなければ、ほとんど使い物にならないような感じであった。

所有関係とEntityGroupについて

 実はEntityGroupと関連に直接的な関係はない。そもそも親子関係をしているのは、JDOのowned関係がEntityGroupとみなされることを利用してトランザクションを実行したいからであった。しかし、そうしなくても、明示的なKeyの生成、つまり、子のキーを親のエンティティのキーを使って生成することでEntityGroupは作成できるのだ。パフォーマンスを出すには、Owendな関連を解除して、Keyの親子関係で関連付けたEntityGroupを使って、トランザクションを実行すればよいことがわかった。



以上の結果を踏まえてコードを修正した。




金曜日, 8月 21, 2009

【Google App Engine】 大量のPDFを生成してわかったGAEの真の実力 このエントリーを含むはてなブックマーク


クラウドPDFサービスのパフォーマンス測定について

 先日公開したScalable PDFのパフォーマンステストと同様に、クラウドPDFサービス(GAEのReflex iTextサービス)についてもパフォーマンステストを行ったので公開したいと思う。
 クラウドPDFサービスは、AmazonEC2のかわりにGAEを使っており、負荷分散機能はGAEまかせとなるが、Reflex iTextのサービスを使っている点はScalable PDFサービスと同じである。ただ、GAEの場合は実行時間など、いろいろな制約があり、リクエストが失敗する前提で設計しなければならないといった考慮が必要となる。今回のテストにおいては、エラー時に時間間隔を置いてリトライする仕組みを取り入れている。
 結果としては、以下のように、GAEでも約8千ページを4~5分で処理できたということで、まずまずであったのだが、これがいっぱいいっぱいであったことも事実であった。1リクエストが80ページ生成で30秒かかることを考えると、少なくとも数十のアプリケーションが並列処理していたのではないかと思われる。(1アプリに複数リクエストを与えると30秒以内で返せなくなりタイムアウトエラーになるため、成功リクエストのほとんどが単独で処理されていたと考えられる)しかし、さらに多くのリクエストを処理させたい場合には、アプリケーションを分けるなどの工夫が必要となってくるだろう。ただ、一回目より二回目の方が明らかにエラー件数が減ったため、徐々にリクエストを増やしていくのであれば、そのままでもスケールしていくかもしれないという期待はある。
 ちなみに、EC2を使って作成する場合には、8000ページを約1分で処理できるが、400プロバイダ/20インスタンス(c1.xlarge)の起動が必要となる。また、レイテンシーの問題があるので実際の処理時間は2~3分といったところだと思われる。
 c1.xlargeは、Opeteron 1.0-1.2GHz相当*20Unitで7GB Memあり、Celelon2.2Ghz、512MBのマシンの10倍程度の処理能力が確認されたが、GAEの場合はCelelonの2~3倍、c1.xlargeの1/4程度の能力であった。

 * エラーがあれば時間間隔を置いてリトライする仕組み(時間間隔=100msec*リトライ回数*10msec)
 * 1リクエストで生成できるページ数の限界は80ページ(処理時間は30秒でこれ以上はタイムアウト)
 * リクエストを100並列で実行するとエラー数が膨大となった(失敗)
 * リクエストを1回目より減らして95並列で実行すると4~5分かかった(成功だがリトライが多いもので8~10回程度発生した)
 * 一回目より二回目の方が明らかにエラーの数は減っている(GAE側でスケールしている?)
 



クラウドPDFサービスとScalablePDFサービスのすみわけ

 クラウドPDFサービスは、当初は、Morph AppSpaceで動作する無償のサービスとして発表したが、GAE上で動作する現在においては、Googleサービスの特長である、素早いレスポンス、自動的にスケールする機能といったものが追加されて素晴らしく進化した。一方、EC2を動作環境とするScalablePDFサービスでは、AutoScale機能+自前の負荷分散プログラムによってクラスタリングを行い、スケーラビリティを実現しているが、複雑になってしまった分、管理コストが重くなってきている。
 これら2つのサービスは現時点において機能的に大きな差はない。強いて言えば、スケーラビリティに関してEC2の方が瞬間的に大きなトラフィックが発生した場合に対応しやすいということぐらいである。ただし、EC2にはレイテンシーの問題が依然として存在している。
 したがって、リトライ処理などを含めた時間的なばらつきや応答時間が許容範囲であれば、現時点でのおすすめは、当然、GAEによるクラウドPDFサービスである。どうしても厳密な応答時間が要求されるのであれば、ScalablePDFサービスをおすすめすることになる。
 利用料は、クラウドPDFサービスは大企業だろうと個人だろうと月額固定の3000円であるが、ScalablePDFサービスは月額数万円程度とさせていただいている。運用管理コストが料金の差となって反映されていると考えていただければと思う。

木曜日, 8月 20, 2009

【おしらせ】 8/24次世代Web活用部会でGAEについて話します このエントリーを含むはてなブックマーク


第2回XMLコンソーシアム 次世代Web活用部会にて、『GAE使いこなしのディープな世界』というテーマで話します。単なるアプリの作り方ではなく、大量データ処理を実際にやってみて、うまくいったこと、いかなかったことを中心に、実践的な内容を話す予定です。

 * 大量のPDFを生成してわかったGAEの真の実力(Scalability、パフォーマンス)
 * 大規模データを問題なく処理するためのテクニック(トランザクション、EntityGroupとRelationなど)

【Reflex】 JSONにおける属性表現、List表現 このエントリーを含むはてなブックマーク


JSONにおけるXMLの属性表現

 JSONではXMLの属性といったものを表現できないため、これまでReflex JSONでも属性を無視していたのだが、AtomをJSON化したくて、Link要素の属性を扱う必要が出てきたので、Reflexでもムリクリ表現できるようにしてみた。Reflex Core に詳細を記述してあるが、V1.2.1から使えるようになっている。(同時に要素名のハイフン変換もサポートした)


* JSONでは、要素名の"-"が"__"に変換されるため"type_str-ing"は、"type_str__ing"となります。
* また、XMLの属性は、JSONでは"要素名___属性名"プロパティとなり、"attr1"は、JSONでは"type_str__ing___attr1"プロパティとして表現されます。


ListをJSONに変換する

 JavaのListをJSONに変換する際に、両者の関係だけにとらわれてしまうと、なかなか思うように作れないので、一旦、JSONとListのことは忘れて、XMLに変換するイメージをもちながら構造化していくといった方法で設計するのがよいと思われる。以下にQ/Aの抜粋を添付する。


List をJSON に変換するのは基本的に構造化が必要です。
以下のサンプルがわかりやすいかと思います。
http://reflex.sourceforge.jp/reflexitextsample_function4.html

EntityはStockInfo.javaで、StockList.javaで値を詰めています。
構造化をイメージしやすいのはstocklist.xmlですが、stocklist.jsonも同じentityから生成したものですので、同じ構造をしているといえます。

Reflexでは構造を意識しないといけない仕様になっていますので慣れるまでちょっと不便ですが、そういうものと思っていただくしかないでしょうね。

ただ、JSONの配列の便利さも損ないたくないので、以下のサンプルのような、配列の要素数
を与えてムリクリ変換できるような方法も用意しています。

http://reflex.sourceforge.jp/reflexjsonsample.html




日曜日, 8月 16, 2009

【Reflex iText】 Publicクラウドはサブスクリプションモデルがいい このエントリーを含むはてなブックマーク



publicクラウドのビジネスモデルは「XX放題」

 Reflex iTextは、先日、月額3000円で利用できるサービスとして発表させてもらった。Reflex iText


Morph AppSpaceやGoogle App Engineなどのクラウドサービスにデプロイして使用する場合は、1インスタンスにつき1ライセンス料が発生します。1ライセンスはトランザクションの量に関係なく固定で月額¥3,000となります。warファイルの提供はありません。基本的に弊社にて導入支援を行います。サービスレベルはMorph AppSpaceやGoogle App Engineに依存します。また、利用料は別途お客様負担となります。


 Reflex iTextがこのような定額の利用料を徴収するサブスクリプションモデルとしたのは次のような理由による。

 7/23に開催された、アカマイ・カスタマ・カンファレンスの「ビジネスを伸ばす次世代Webとは何か?」というパネルディスカッションで知ったのだが、アテインさんの動学.tvという教育サービスは月額2800円で受講し放題である。これが割と繁盛されていて、現在も会員さんをどんどん増やしているらしい。月額固定で借り放題といえば、TSUTAYAのDISCASなんてのがあるが、これとよく似たサービスだ。モデレータの佐々木さんは、次世代Webのビジネスモデルは、サブスクリプションモデルと広告モデルが中心になるだろうとおっしゃっていたが、たしかに、月額定額利用料で「XXし放題」というのはとても魅力的なモデルである。これまでクラウドもユーティリティーコンピューティングというキーワードで電気やガスのように利用するモデルが吹聴されてきたが、要するに、サブスクリプションモデルのことである。どんなに使われても非常に安く提供できるクラウドがあればこそサブスクリプションモデルも生きる。

受託開発でGAEを利用することは可能か


 publicクラウドは、大企業から請負で開発するというビジネスモデルより、コンシューマをターゲットに小売的な商売を行って、使ってもらってナンボといった感じのビジネスモデル方が合うような気がしている。クラウドが請負になじまないのは、最大の壁であるセキュリティ問題があるからだ。セキュリティは最後の難問題でも触れているが、クラウドでセキュリティを確保することは一筋縄ではいかない。メーリングリストで以下のような議論が行われていたので、SLAと担保の2つをどう解決するかという自分なりの意見を述べてみた。


受託開発でGAEを利用することは規約的に可能でしょうか?
またデメリットとしてどのようなことが考えられるのでしょうか?


>情報漏えいやシステムダウンは、どんなに注意を払っていてもおきるときはおきます。

私もそう思います。
ただ、実際には、Googleは免責されている(事故が起こっても損害金額などを払ってくれない)ので、発注者側がリスクを負うことになってしまいます。

少ない確率であっても、実際に情報漏えいやシステムダウンが起きる可能性があるため、発注者は導入の際に、事故が起きたときに誰がリスクを負うのかということを、会社に説明できなければなりません。(私も数年前までは発注者側にいたので、少なくとも会社はリスクを負わないということを、役員会などで説明する必要がありました。私に限らず皆そうなんだと思います)

その点、アウトソーサが管理しているシステムであれば、サービスレベルの質はどうであれ、彼ら自身が担保してくれている(個人情報漏洩であればほぼ無限責任)ので、発注者側はとても楽です。ただ、この形態は、ものすごく高くつくので、本当に会社の利益になっているかどうかは、経営者自身がよく考える必要があると思っています。Googleのシステムだからとか、彼らは優秀だからといった形容詞を使ってリスクをいかに少なく示したところで、担当者による説明はとても難しいと思いますので、結局は経営者判断になるんじゃないかと。

最近、私は逆の立場になり、GAEを提案しています。

お客様の意見としては、安さが会社の競争力になるという評価をしていただいている一方で、やはり、セキュリティの課題をクリアーできずに導入できないというところが多いようです。
また、抵抗を示している多くがIT部門なのもおもしろいと感じています。彼らは、個人的には導入にものすごく意欲的なんですけど、大企業だと会社ルールの壁を突破できないというか、政治的に動く必要があって、それがなかなかできないみたいです。事例もない現時点では無理もないことだとも思います。

結局のところ、大企業に浸透するまでには時間がかかりそうだから、中小企業か個人をターゲットに数多くのユーザを獲得する以外なさそうかな、と私は感じています。コンシューマをターゲットに小売的な商売を行って、請負でいくらというより、使ってもらってナンボといった感じの方が合うような気がしています。


水曜日, 8月 12, 2009

【雑記】 イケメンとチョイワルの喧嘩 このエントリーを含むはてなブックマーク


 今回の喧嘩ネタはちょっとレベルが高い。(見方によっては低い)

 福岡の夜 (shi3zさんとの喧嘩)
 そりゃブログを書くだけで平和が来るなんて甘い夢など見ちゃいねえさ

 リーダシップを論じるお二人の主張が異なるのはさておき、shi3zさんがid:mkusunokさんに最も伝えたかったことは「ブログの無力さ」であり、1個人がブログに書くということが、いかに微々たる力であるかを認識すべきであるということだった。それを、「お前みたいなクズのような存在」と相手に直接いうことでわからせようとした。(たぶん、この表現自体に問題があると思われるが、ストレートに表現することを信条としているshi3zさんにとっては普通のことらしい)

 一方、普段からshi3zの言動が気になっていた小野さんはこれにブチキレ。以前、「嘘を平気でつける人間」であり、それ故に「欺瞞に満ちた人間」であると言われていることもあって、「お前はいままで、いったい何をしたんだ。何もしてないクズだろ」となった。

 shi3zさんは本当のことを伝えることが「善」であり「手段」を選ばないこともある。一方の小野さんは「手段」を選ばないと損するよ、と伝えたい。

 どちらも正しいことをしているように思えるが、ぜんぜん会話になっていない。コミュニケーションを阻んでいる原因は「妬み」と「欺瞞」なのだろう。面白いのは、自分は妬んでいるといっているshi3zさんがいる一方で、自分は欺瞞であるとはいえない小野さんがいること。

 また、今回の件で相当な決意とやる気をだしたshi3zさんの未来は明るいかもと思った次第。

木曜日, 8月 06, 2009

【Google App Engine】 Entityとトランザクション2 このエントリーを含むはてなブックマーク


 今回は親子関係をもったEntityのUpdateを中心に解説する。例によって、請求書アプリを題材にしている。ソースは=>ココで、使用しているReflexGaeライブラリは=>ココ

親子関係をもつEntityのUpdate

 請求書アプリでは、請求書1枚がEntity1インスタンスとなるように設計されている。1つの請求書(Invoice)が複数の明細レコード(Order)をもち、さらには、Invoiceを親であるInvoiceBaseがListとして複数保持している。また、InvoiceBaseはInvoiceのレコード数、および、Orderのレコード数も保持している。このような関係をownedというが、このような構造をしているのは、EntityGroupとして定義しているからで、1トランザクションで実行する必要があるからである。
 これらを更新するには、下図のように、まず、クライアントから受け取ったJSONのInvoiceをオブジェクトに変換したものとDatastoreとの差分を取り、その差分を反映させたオブジェクトをあらためてPersistentにすればよい。



DatastoreをUpdateする際の注意点


* 更新でmakePersistentは必要なし

 Datastoreから取得したオブジェクトはPersistentな状態である。Persistentな状態とは、Datastoreへのアクセサを保持している状態で、setterを使って値をセットするだけで、(makePersistentを呼び出さなくても)永続化されることになる。
 また、所有関係にあるオブジェクトがデータストアに保存されると、そのオブジェクトと関係があるすべてのオブジェクトの内、保存する必要のあるもの(新しいオブジェクト、または最後に読み込まれてから変更されたもの)は自動的に保存される。つまり、Persistentなオブジェクトにaddすることで、親のオブジェクトは子を含めてPersistentになる。

* lazyな読み込み

Datastoreから取得したオブジェクトの子は eager loadされない(アクセサを使って参照したときのみ値を取得できる)ので、別途、子を取得して親にセットする必要がある。

 * propertyの更新は1トランザクションで1回のみ許される

2回以上更新すると、「can't update the same entity twice in a transaction or operation」というエラーが起きる。

* オブジェクトの親子関係とKind(≒Table)の親子関係に関係はない

親オブジェクトに子をaddしたからといってKindの親子関係になるわけではない。Kindの親子関係を紐付けるのはKeyであり、子のKeyに親のKeyをAddすることで関係付けることができる。Keyを明示的にセットしない場合(nullの場合)はシステムが勝手にKeyをセットする。

サンプルプログラム

 
 以下のように動作するサンプルプログラムを示す。
1.でそれぞれ取得しているのは、InvoiceBaseからInvoiceを直接取り出すのができないから。(forループでまわさないと取れない)また、lazyな読み込みなので、3.の処理が必要となる。
2.でdetachCopyしているのは、Persistentオブジェクトを直接操作するのを避けるため。Persistent直接だと、setterを1回しか呼べないという制約にもひっかかる。


1.InvoiceとInvoiceBaseのPersistentオブジェクトをそれぞれ取得する
2.invoicePersistentから、いったんdetachCopy
3.コピーしたものに子要素のorderを追記補完
4.invoiceからinvoiceSource(1品一葉)を取得する。
5.クライアントからのリクエストデータで更新があったものだけをinvoiceTargetにコピーする
6.Revisionを更新
7.InvoiceBaseにaddすることでInvoice以下子要素のorderなどもpersistentになる





AJAX CRUDサンプルとJDO代替ライブラリ
RESTfulアプリのCRUDサンプル -Modeling編-
RESTfulアプリのCRUDサンプル -Servlet編-
Entityとトランザクション

水曜日, 8月 05, 2009

【iPhone】 AppleのiPhone向けのアプリビジネスはなぜ魅力的ではないか? このエントリーを含むはてなブックマーク


GoogleのAndroid向けのアプリビジネスはなぜ魅力的ではないか?をちょっと書き換えてみた。


Android向けの開発意向表明を行って数ヶ月になるが、もっとも良く投げかけられる質問は「iPhoneへのプラットフォームでは開発しないの?」というものだ。
少し前までは、ObjectiveCの開発スキルやリソースに目処がつけばやってもいいけど、学習コストが重いよねえ、などと答えていたが、最近は少し見方が変わってきた。

今の勢いでHTML5が進化・浸透してくれるのであれば、わざわざ移植コストをかけてiPhoneやWindows Mobile向けにネーティブ・アプリを開発するよりは、少なくともUIの部分をすべてHTML+Javascriptにまかせたアーキテクチャでのインタラクティブなアプリの開発というのも十分に可能性があるように思えてきたのだ。

この「HTML+Javascriptですべて出来るじゃん」という発想は、そもそもマイクロソフト時代のInternet Explorer 4.0やNetDocs(90年代の終わりに開発していたマイクロソフト版Google Docs)のころから持ち合わせて来たもの。パソコン向けではWindowsの成功が故に日の目を見なかった発想が、数多くのプラットフォームが混在・競合しているスマートフォンの時代になって大きな意味を持ってきたというところが興味深い。

 そもそも、「AppleがiPhone」を発表する、と聞いた時には、「ついに超高速・超小型のJavascriptエンジンの開発に成功したか」と過大な期待を抱いてしまった私にとって、ObjectiveCでNativeコードを書かなければならないiPhoneははっきり言って期待はずれの拍子抜け。明らかにGoogleの「ウェブアプリケーション中心」というの基本路線から外れており、せっかくiPhone向けのアプリを作った開発者がAppleに途中ではしごを外されて途方にくれる可能性は大きいと私は見ている。

現時点で、最新のハードウェア向けのアプリを作りたければAndroidがiPhoneよりも魅力的なプラットフォームなことは火を見るよりも明らかだし、どうしても「Googleに縛られたくない」のであれば、標準になりつつある、HTML5上でJavascriptを駆使した本気のインタラクティブなアプリを作るノウハウを蓄積しておく方がよほど魅力的に思える。もちろん、現時点でもっともすぐれたHTML5の実装を持つモバイル端末は iPhoneだし、ちゃんと作っておけば、Android端末でもPalm Preでも動くはずなので、投資効率は高い。

 ということで結論から言ってしまえば、「移植性を無視して最新のハード向けにばりばりのネーティブコードを書きたかったらiPhone向けのアプリを Objective Cで作るか、Android用にJavaで作り、さまざまなデバイスへの移植性が重要ならHTML5+Javascriptでインタラクティブなアプリを作ってiPhone上のSafariでテストしておく」というのが現時点でのスマートフォン向けの開発投資の仕方としては、最も賢い選択肢だと考えている私である。

ちなみに、JavaとかFlashとかはどうなの、という質問が来そうなので答えておくと、「スマートフォン向けではなく、現状の端末向けのアプリの開発であればまだまだJava、BREW、Flash liteは健在。ただし、3〜5年後にそれらのプラットフォームがHTML5によって駆逐されてしまう可能性は大」というのが私の見方。もちろん、「ガラパゴス携帯」とか「Windows Mobile」という局所的な抵抗勢力はそれなりに残るだろうけど、WebKitがモバイル端末でさくさく動くことをAppleが証明してしまった今、世界の流れはHTML5に一気に向かっている。

日曜日, 8月 02, 2009

【Google App Engine】 RESTfulアプリのCRUDサンプル -Modeling編- このエントリーを含むはてなブックマーク


 前回に引き続き、RESTfulアプリの解説をおこなう。今回はModeling編。本当は最初に説明すべきところ。

Entity設計

 外部設計において基本となるのはBCEモデル。3分で分かる設計の話 にもあるように、Entity設計とは項目の整理をすること。項目の抽出は画面設計を行ってからじゃないとできないので、実際には、Boundary=>Entity=>Controlの順番で設計を進めていくことになる。Entityの設計では、基本的には名前と型と多重度の定義を行うだけでよいのだが、もう少しがんばって、具体的にXMLのインスタンス(項目とデータを含むもの)まで作成するとよいと思う。ちなみに、請求書アプリのXMLインスタンスはこんな感じになった=>XML

 ReflexではModel設計のことを敢えてEntity設計と言うようにしている。Modelは型で、Entityはその型から作ったインスタンスであるから、Entityは金太郎飴の一粒のようなもの。Reflexでは、Entityのインスタンスイメージの方が具体的で好都合なので、なるべくそう呼ぶようにしているが、設計フェーズを意識しないときは、Model設計とEntity設計はよく同じ意味で使ったりもするので、実際にはそれほどこだわっているわけでもない。ただ、Modelを強調してしまうと、「Model=抽象的な器+ロジック」のロジック部分が強調されることになって、データ構造を中心に考えるReflexの設計に合わなくなってしまうので、やっぱりEntity設計と呼ぶのがいいような気がしている。
 
Modelの設計とサービス化
 
 Entityの設計ができたら内部設計をすすめてModelを設計していく。Modelには、データ構造を表現する部分と、ロジック(ビジネスロジック、または、ドメインロジック)を表現する部分から成るが、Reflexではこれを2つに分離させることを推奨している。具体的には、データ構造を表現したModelに実際のデータの管理をまかせてCRUDのAPIを持たせることが一つと、ドメインロジックには、専らビジネスロジックの実行処理をまかせるのが二つ目。ビジネスロジックの実行では、例えば、Entityから別のEntityへの変換や、HTMLやPDFなどへの変換、メール送信などが考えられる。前者はDatastoreへの登録があるから不揮発性なのに対し、後者は揮発性であるのが特徴である。ドメインロジック内で許されるのは参照のみで、登録や更新は絶対に許されないとものする。ただし、Datastoreからの参照は可能であり、必ずしもWebサービス経由で参照する必要はない。
 また、Modelのこれら2つの機能をREST化したものをサービスと呼んでいる。例えば、EntityをCRUDで操作するデータ管理用のサービスと、PDF変換などのロジック実行用のサービスなどがある。これらはModelの2つの性質をサービス化したものと考えることができる。

サービスにおけるEntityの扱い方と具体例

 RESTfulな新3層アーキテクチャでも説明しているとおり、すべての操作はパラメータにentityを与えることにより実行する、というポリシーで設計する。こうすることで、entityという一貫したI/Fが扱えるようになり、その結果、外部設計の目的が明確になる。外部設計とは、つまり、entity設計を意味することになる。


 entity = create(entity);
 entity = retrieve(entity);
 entity = update(entity);
 entity = delete(entity);


 請求書アプリで具体的に説明してみよう。まず、create(entity)に対応するのは、invoiceのXMLをPOSTメソッドで実行する部分である。Servlet内部では、reflexgaeライブラリを使ってXMLをrequestオブジェクトから取り出してinvoiceオブジェクトに変換、それを直接datastoreに挿入している。非常にシンプルな実装である。<参考>RESTfulアプリのCRUDサンプル -Servlet編-


 Invoice invoice = (Invoice) getEntity(req, MODEL_PACKAGE);
 jdoUtils.insert(invoice);


 retrieve(entity)に対応するのは、以下の部分で、requestオブジェクトのクエリパラメータの内容を、一旦、Invoice paramに詰めて、それを条件にjdoUtils.getEntriesByParam()で検索を実行している。


 Invoice param = new Invoice();
 (new RequestMapper()).setValue(req, param);
 ・・
 jdoUtils.getEntriesByParam(param, pagesize, nextid);


 update(entity)も基本的に同様であり、PUTされてきたXMLをInvoiceに変換して、jdoUtils.update(invoice)を実行するだけである。delete(entity)も同様である。


 Invoice invoice = (Invoice) getEntity(req, MODEL_PACKAGE);
 JdoUtils jdoUtils = new JdoUtils();
 jdoUtils.update(invoice);


 jdoUtilsの内部では、reflexgaeの FieldMapperを使って、targetのinvoice(persistant)に対して、sourceのinvoiceの更新があるプロパティだけをセットするようにしている。つまり、sourceのinvoiceのプロパティのうち、nullでなく、かつ、targetと異なる内容のものを、setterを介してセットするという処理をfieldMapper内で行っている。わざわざsetterを介すのは、JDOに更新であることを認識させるためである。(フィールドを直接書き換えると更新されないので注意)
 また、invoiceのように、1:nの子要素を持つ場合でも、この操作1回ですべての子要素を更新することができる。ただし、子要素のKeyはしっかりsourceの方にセットしておく必要がある。(私が試したのは2階層までであり、3階層の孫要素も更新されるかどうかはわからないので注意。)


fieldMapper.setValue(invoice,target);


 

Control

 Controlは、簡単に言うとサービスを呼ぶ機能のことである。他のサービスを呼びつつ、一連のフローを実行して、別の新たな結果を返すような機能、といった感じだろうか。いわゆる、マッシュアップがそうだし、先日公開した、Google Spread SheetからPDF生成するBookmarkletもControlの一種である。ドメインロジックをサービス化したものはControlになることができるが、すべてがControlと呼べるかというとそうではない。

Boundary

 いわゆる画面のことである。本当はこれを最初に説明すべきところだが最後になってしまった。ここでいいたいのは一つだけ。レンダリングのために必要な処理はBoundary内に記述すべきということ。EntityをサービスからXMLやJSONで受け取ってレンダリングしたり、あるいは、GEARなどに一時的に保存したり、サービスに登録内容を送信したりする処理は、Boundaryとして実装すべきであり、決してControlなどに押し付けてはいけない。画面内に閉じることで疎結合のメリットを最大限に享受できるようになるからだ。

まとめ

 ここまで、主に外部設計の話とBCEモデルについて説明してきたが、あらためて強調したいのは、Entity設計をクローズアップするということ。つまり、Entityの設計をしっかりやって、それを一貫したI/Fとして扱うべし、ということにつきる。サービスについても、EntityをパラメータとしたCRUDや、Entityを与えてEntityを返すという単純なものにすべきである。そう考えていくと、Entityさえできれば、それをもとに自動的にscaffoldを作成するなんてことは簡単にできる。ちなみに、Reflexでは、reflexentityeditorというものを用意しており、Reflex表現からEntityを自動作成するツールも用意している。将来的には、CRUDができるServletや、HTMLページなどを生成するscaffold生成機能も用意したいと考えている。しかし、今現時点では、まだ他に優先してやるべきことがたくさんあるので取り掛かることはまだしない。

 最近知ったのだが、これに非常によく似たものに、Simple Modelerというものがある。モデル駆動開発を掲げ、Scalaで作成したメタデータを元にモデルを自動生成できる。

 Reflexは割と古くて2005年ぐらいから存在しているが、そのコンセプトは黙殺されてきた感がある。(別に流行らなくても結構なのだが)
 最近、同じようなことを考えている方が世の中にもいるということで、ちょっと安心した感もある。だが、Simple Modelerが大流行りして、Reflexが真似しているといわれないようにしないといけないなあとは思う。

木曜日, 7月 30, 2009

【Google App Engine】 RESTfulアプリのCRUDサンプル -Servlet編- このエントリーを含むはてなブックマーク


 今回は、RESTfulアプリのServlet部分について説明する。例によって、請求書アプリを題材にしている。使用しているReflexGaeライブラリは=>ココ

検索(GET)

 検索ではRequestパラメータを受け取り、それを条件にdatastoreを検索して、結果をJSONもしくはXMLで返す。

 1. ReflexServletをextendsしてRESTful機能をもたせる
 2. RequestパラメータをEntity(Invoice)にセットする
 3. パラメータInvoiceを与えてdatastoreを検索
 4. 結果をJSONもしくはXMLにして返す
 5. エラー発生時はエラー内容をJSONもしくはXMLにして返す




登録(POST)

 登録では、クライアントからPOSTで受け取ったXMLもしくはJSONをEntityオブジェクトに変換した後、datastoreへのINSERTを実行する。クライアントには登録成功でHTTPStatus201(created)を返す。


 1. クライアントから受け取ったXML(JSON)をEntityオブジェクトに変換
 2. すでに登録されている場合エラー
 3. 登録されていなければinsert
 4. エラー発生時はエラー内容をJSONもしくはXMLにして返す




更新(PUT)

 更新では登録と同様に、クライアントからPOSTで受け取ったXML、JSONをEntityオブジェクトに変換してUpdateを実行する。更新オブジェクトのRevision番号が同一であれば更新実行、異なれば楽観的ロック失敗とする。



削除(delete)

 削除では、リクエストパラメータをもとに削除したいオブジェクトを取得し、deleteを実行する。該当するオブジェクトが存在しない場合に、HTTPStatus 204(no content)を返すようにしているが、これは特になくてもよい処理かもしれない。



嵌りどころと注意点

 1. POSTの場合、Requestに対し、getInputStreamを実行する前に、getParameterを実行してしまうとInputStreamの値が読み取れない。(PUTの場合はなぜかうまく動作する)
 2. HTTP Status 200,201以外をセットする場合、ResponseのOutputStreamに内容が出力されない。具体的には以下のような現象となる。


(HTTP Status) : (現象)
* 204 : 内容なし
* 409 : java.io.IOException
* 404 : java.io.FileNotFoundException


 最後に一言。GAEにおけるアプリケーション開発で特に重要だと思うのはテストである。テストコードを書けとか、TDDをやれとかいう意味ではなく、実際にテストして動作を確認することが重要である。(私は自分が作ったアプリのテストコードはほとんど書かないが、他人が作ったものは実際に検証しないと何も信じないタチである。)実際に現象を確認しないで、SPECやDocumentを見て判断してはいけないし、やみくもに信じてはいけない。ある程度予想できる結果であったとしても、必ずテストして確認すること。GAEは特にそうしないと危ない。石橋を叩いて渡るぐらいの心構えでちょうどよいと感じている。

<関連>
AJAX CRUDサンプルとJDO代替ライブラリ
RESTfulアプリのCRUDサンプル -Modeling編-

火曜日, 7月 28, 2009

【Google App Engine】 Pagingをどうやって実現するか このエントリーを含むはてなブックマーク


 JDOをそのまま使うとPagingができないことは前記事で述べたとおりだが、DatastoreAPIを使えば可能なので、今回はその方法について述べたいと思う。前記事と同様、請求書アプリを元に説明する。

KeyとCounter


 まず、基本となるEntityのInvoiceBaseとInvoiceの構造から。



 InvoiceBaseは、Invoiceレコードを保持するする親のEntityだ。InvoiceBase(親)に紐づくInvoice(子)は1:nのOwnedな関連である。また、Invoiceレコードの件数を格納するcounterプロパティをもつ。InvoiceのKeyは、トランザクションで括る必要があってKeyを連結させているが、拙作ライブラリReflex GAEのAPI、keyUtils.getChildKey()を使って、連結されたInvoiceのKeyを取得している。(ReflexGaeライブラリは=>ココ
 レコードの件数counterは、Pagingのために必要な連番を振るためにも使用される。Datastoreは自動で連番を付けられないため、InvoiceBaseの自身でもつより他ない。

 一応、JDOのドキュメントで以下のようなものがあったので試してみたが案の定できなかった。(追記:最新版SDK1.2.6では、Statics APIで件数が取れるようになっている。こちらの記事の参考に=>Keyとカウンタは別々に考えるといいかも


【コード】
 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.INCREMENT)
 private Long uid;

【エラー内容】
 Message : There is no available value generator for strategy "increment" for this datastore. Please consult the documentation for details of which generators are available.


 そもそも、親子関係をもつようなEntityを扱うには、Long型ではなくKey型を使わなくてはならない。Googleのドキュメントにも記述されているが、Keyの中身が以下のようなツリー構造をしていることからもその理由は想像できる。


子要素までaddしたときのキー(実際はBase64でエンコードされている)

 j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID2
 j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID1


 よく、KeyをStringとしてもたせるような記述、「@Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")」を見かけるが、階層型の場合、連結させたDatastoreのKeyがそのままStringに変換されてしまうのでおもしろくない。例えば、先のEntityのKeyをStringにするとその中身は、「j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID1 」となってしまう。業務アプリで意識すべきは、業務アプリのKeyであって、DatastoreのKeyではない。DatastoreのKeyはむしろ隠蔽させて、ID1という業務アプリのキーだけを意識させる方がわかりやすいだろう。なので、Reflexでは、JDO KeyをStringとしてもたせる記述はせず、アプリのPKをKeyに変換するという方法をとっている。以下はアプリのPKであるinvoiceNoをJDOのKeyに変換する例である。(keyUtilsはReflexGaeのAPI)


 Key childKey = keyUtils.getChildKey(Invoice.class, param.invoiceNo);


Keyによる検索

 このPKを使った検索の例は次のとおり。pm.detachCopy()をすることで、Persistentではない、自由に扱えるEntityのコピーを得ることができる。Persistentなものは、まだDatastoreにAttachされていて、コネクションが張られている状態のような感じ。実際に値を書き換えるだけでDatastoreの中身も変更される。



Paging検索

 Paging検索では、以下のように、idで開始点を与えて、かつ、withLimitで範囲を絞るといった感じになる。JDOでは6千件でフリーズしてしまったが、DatastoreAPIを使っているため理論的には無限で、このサンプルではcounterの最大値(longの2147483647件)までは扱えることになる。とりあえず1万件でテストしてみたが非常に高速にレスポンスが返ってきたので問題なく使えると思う。

 1) filterで開始点となるidの値をセットして、それ以上のものを検索対象とする

query.addFilter("id", Query.FilterOperator.GREATER_THAN , Long.parseLong(nextId));

 2) FetchOptionsでwithLimit()をつけることで、検索結果の件数を絞る

FetchOptions fetchOptions = FetchOptions.Builder.withLimit(limit);




指定可能なfilter条件

 DataStoreAPIでは、GREATER_THANなどのinequality filter(<,>など)が1つと、equality filter(=)を複数指定できる。連番でinequality filterを使ってしまっているため、あとはequality filterだけが使えることになる。Reflex Gaeライブラリでは、QueryUtilsというものを用意していて、検索条件を格納したParameterBeanからequality filterを自動的に追加できるようにしている。

 // 検索項目(Indexがあるもの) を指定してnewする
 QueryUtils queryUtils = new QueryUtils(new String[]{"invoiceNo","companyName","job","issuedDate"});

// paramの検索項目がnullでなければaddFiler(FilterOperator.EQUAL)される
queryUtils.setParam(param, query);


EntityConverterによる変換

 DatastoreAPIで検索すると、MAPで返ってくるのでEntityのPropertiesからListに変換する必要がある。DatastoreAPIには、DataTypeTranslatorというのがあったが、使い方がよくわからなかったので自前で作成した。それがEntityConverterである。

List result = entityConverter.convert(Invoice.class, resultIterable,null,condition);


 filterで指定できる条件はequality filterだけなので、Like検索など複雑なことをやりたい場合には、Entityを取得した後で別途Java側で処理しなければならない。EntityConverterにソート機能や条件抽出機能をもたせたているのはこういった理由からだ。
 comparatorをEntityConverterの第三パラメータに与えることでEntityをソートして返すことができる。また、EntityConverterの第4パラメータにconditionを与えることで、conditionに合致するものだけを変換対象にするといったことができる。
 
Indexについて

 上記ソースのQueryはDatastoreAPIのものであってJDOQLではないので注意が必要だ。JDOQLでは自動的にIndexが作成されるが、DatastoreAPIではindexがないとエラーになるので自分で作成する必要がある。(これはむしろ好都合)
 作成するには、すべての検索パターン分のindexを定義した、datastore-indexes-auto.xmlを所定の位置(WEB-INF/appengine-generated)の下に置いてDeployするだけだ。(datastore-indexes.xmlではダメだった。また、index作成には結構時間がかかるので辛抱づよく待つ必要がある。管理画面のDatastore=>Indexesで、StatusがServingになればOKだ。とにかく待とう)
 すべての検索パターン分のindexとは、検索条件の組み合わせをすべて定義しなければならないという意味である。例えば、請求書アプリでは、"id"と"companyName"の2つの条件で検索したい場合もあれば、"invoiceNo","companyName","invoiceNo","issuedDate"の4つで検索したい場合もある。検索する可能性の組み合わせすべてを定義しなければならない。



登録処理 

 登録では次のような感じになる。



大規模なデータをもつ子要素に対してInsertして問題ないか

 まず、pm.getObjectById()でinvoiceBaseを取得する。存在しなかったら新規作成して、pm.makePersistent(invoiceBase)を実行する。これは更新時には必要ないため新規作成時のみ実行するようにする。Invoiceレコードを追加するたびに、InvoiceBaseのcounterをインクリメントする。
 InvoiceレコードはinvoiceBaseの子要素であるが、いわゆる、eager loadではないため、pm.getObjectById()しただけでは、Invoiceの全レコードがメモリに乗ることはない。getter/setterでアクセスしたものだけが実際のDatastoreへのアクセス対象となるので、膨大なレコード数が対象であってもこれでOKだ。(実際に1万件に対して実行しても大丈夫だった)

パフォーマンス

 パフォーマンスへの影響を考えた場合、EntityGroupは最小化すべきとよく言われる。実際、この例はテーブルロックのイメージに近いため、RDB設計に携わってこられた方は、どうしても気になるところだろう。counterとレコードの関係を考えると、トランザクションは切り離せない部分であるため、どうしてもこのような実装になってしまうが、それでも、コンテンションが起きた場合は、JDOCanRetryExceptionを拾ってリトライできるので、スループットは出せるのではないかと思う。実際に測定してみたところ、遅くなる要因としては、EntityGroupの大きさというより、トランザクション処理の件数の方がインパクトが大きかったので、TaskQueueとMemcacheを駆使して、複数のトランザクションを1つにまとめて非同期に一括処理するようなものを介すことでパフォーマンスを改善できると思われる。このあたりの話は深いのでまた別の機会に詳しく述べたいと思う。
 ちなみに、上記コード中で、JDOCanRetryException内で都度rollbackしているのは、そうしないとうまく動作しなかったからだ。(Documentどおりだと動作しないと思われるので注意)

楽観的ロック

 同一レコードの書き換えでコンテンションが起きてしまう場合は楽観的ロックが有効である。@Version(strategy = VersionStrategy.VERSION_NUMBER)を使用する方法があるようだが、この程度のものは潰しがきくので自前で実装する方がよい。具体的には、Updateの際にrevisionを比較して同じであれば更新実行してrevisionを+1するだけ。比較して違う場合にエラーとすればよい。

<関連>
Entityとトランザクション
JDOから直接JSON、XML
JDOから直接SOAP、ATOM。それからDeep Copy
AJAX CRUDサンプルとJDO代替ライブラリ
RESTfulアプリのCRUDサンプル -Servlet編-
RESTfulアプリのCRUDサンプル -Modeling編-

土曜日, 7月 25, 2009

【Google App Engine】 AJAX CRUDサンプルとJDO代替ライブラリ このエントリーを含むはてなブックマーク

RESTfulなWebサービスとワンソースマルチビュー

 先日行ったプレゼンの請求書DEMOでは、以下のように、HTMLの請求書を直接編集後、PDFを表示させるというところをお見せした。(以下のHTMLを表示させて右クリック、Searchで何かを選択してデータを読み込む、さらに右クリックでPDFを選択して生成)

 請求書デモ








 これは一見、統合されたアプリのように思えるかもしれないが、単なる疎結合なサービスの組み合わせであるにすぎない。つまり、JSONやXMLでCRUDできる機能だけが実装されたRESTfulなGAE上のサービスと、そのサービスに対してAJAXでアクセスするHTMLとJavaScript。そして、GAEのサービスからXMLを取得してPDFを生成するReflex iTextサービスの3つの組み合わせで成り立っているものである。特筆させていただきたいのは、ブラウザで表示させたHTMLとPDF生成で使ったテンプレートHTMLは全く同じものを使用しているという点。GAEのサービスから取得したものであるから、データはもちろん同じものである。このように、Reflexは、ブラウザやPDFなど様々なViewに対して、同じEntityを与えさえすれば、同じように表示させることができる。これをワンソース・マルチビューという。(そもそも、1つのEntityから複数の写像をとれるという意味で、名前をReflex(写像)としている)
 このように、ブラウザ以外にも、携帯やiPhone、Android、Flex2などの異なるメディアや端末に対象を広げられれば、コミュニケーションの幅を簡単に広げることができるため、ワンソース・マルチビューは今非常に重要視されている概念となっている。また、 Chrome OS戦略とIT世界の対立軸 でも述べているとおり、Googleは、RESTfulなWebサービスを基本とすることで、コミュニケーションに革命をもたらすとまでいいきっている。

Entityをそのまま表示させる

 これまで説明したように、Reflexではサーバで画面を作ることはしない。JSPも使わない。こうしてしまうと、どうしても密結合なアプリになってしまう。これは非常に大きなデメリットである。Reflexでは、JSONやXMLのCRUDだけが行えるサービスを基本とするが、JSPも使わないとなると具体的にどのようにブラウザで表示させればよいかが問題となる。この点について説明しよう。そんなに難しく考えないことがポイントである。

 まず、HTMLでモックを作り、表示させたいタグにIDを振る。画面が出来たら、IDを全部抽出して並べてルートタグで括る。これが表示用エンティティ。あとはそのエンティティをJavaでも実装して、AJAXを使って受け取れるようにする。JSONだとeval()できるので楽である。あとは、evalした個々のデータをHTMLのidに対してJavaScriptで挿入すればよい。具体的には、DEMOのinvoice.htmlとdisplay.jsを見ていただくのがよいと思う。ちなみに、Reflex iTextのデータ挿入もほぼ同じやりかたで行っている。また、高速化プロジェクト その1で述べたように、パフォーマンスも特に心配することはない。

 ただ、何でもかんでもこのスタイルがよいわけではないことも付け加えておく。SEO対策などで、サーバが出力する最初の段階から完全なHTMLでないとまずい場合などがあるからだ。そういう場合は、Reflex iTextのように、テンプレートとXMLから完全なHTMLを生成するようなサービスを介すとよいと思われる。いずれにしても、画面をベタ書きして疎結合を損なう設計になることだけは避けたいところである。

JDOとスケーラビリティ

 ここからが本当に言いたいところ。

 我々が検証した限り、JDOは大規模データを処理できないことがわかっている。正確には、JDOQLであるが、たった6千件のデータでさえ検索することができないのである。Indexが勝手に作成されてIndex爆発が起きる問題など、いくつか致命的なものもあるが、最も酷いのは、検索対象を絞るページングなどの範囲指定ができないことである。実は、JDOQLは5千件ぐらいまでは何とか検索できる。あれ?上限は千件までじゃなかったっけ?と不思議に思う方はGAEをよく知っている方。そう、一度にFetchできるのは千件までなのだが、それはDatastoreAPIの話。上位実装であるJDOではなんと5千件一括検索ができてしまうのだ。余計なことに、そんなことができる反面、6千件だともうフリーズしたように無反応になる。setRange()を指定してもダメ。getObjectById()やequalの検索条件を指定してやっと1件取得できるのみ。keyの複数件検索も未サポートなので、もう、これは使い物になりません。(※ただし、トランザクションや更新系はJDOのPMで問題なく使えている)
 そこで、私たちはJDOQLを諦め、低レベルのDatastoreAPIだけを使った検索ユーティリティを作成することにした。これを使えばPagingもできて大規模データの操作も問題ない。先の請求書DEMOも使用している。次回はこの中身について説明していきたいと思う。

<関連>
Reflex GAE
Entityとトランザクション
JDOから直接JSON、XML
AXIS2のデータバインディング能力
JDOから直接SOAP、ATOM。それからDeep Copy
Pagingをどうやって実現するか
RESTfulアプリのCRUDサンプル -Servlet編-

 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。