ラベル Google App Engine の投稿を表示しています。 すべての投稿を表示
ラベル Google App Engine の投稿を表示しています。 すべての投稿を表示

2013年3月1日金曜日

GAE/G+AngularJSでGoogle+ SignInを利用する(1)

最近、公開されたGoogle+ SignInという、Google+アカウントを利用した認証
サービスを利用してみようと思ってがんばってみました。

Google+ SignInについては以下の記事をご覧ください。
http://googledevjp.blogspot.jp/2013/02/google-sns.html

で、本当は公開されているサンプルを動かせばいいのに、わざわざ苦戦する方向で
試してみました。

事前準備として、APIsConsoleで

  1. Google+ APIをONにする。
  2. ClientIDを作成する。(Web Applicationのものを作成してください。)

ClientIDは削除できないので選択を間違えると、無駄にIDが増えていくことになるので気をつけてください。(リダイレクト先は後でも変更可能です。)

ライブラリが提供されているので、実際にはHTMLだけ作成すればローカルのWebサーバを
経由するだけでGoogle+ SignInボタンは表示されます。

実際にSignInボタンを押すと、プロフィール情報が取得され、ブラウザで表示させる事が
できるようになります。

サンプルコードはgithubで公開しています。
https://github.com/tyokoyama/gplus-signin-sample

単純な例であれば、すぐ作れるようになりますが、私はAngularJSも一緒に使おうとして
だいぶはまりました。(SignIn直後にプロフィール情報を表示させる事は今も解決していません)

最初のSignIn後のリダイレクト先にAngularJSのコントローラの関数を指定できないので
以下のように強引に関数を呼び出すか、


var elem = document.getElementById('viewResult');
angular.element(elem).scope().signIn(authResult);

githubのサンプルコードのように、ボタンを配置してボタンを押した後で
プロフィール情報を取得しにいく。という事なら何とか可能でした。

実際の動きはPHPのQuickstartを見ると良いかもしれません。
(HTMLの部分はjQueryが使われています)
https://developers.google.com/+/quickstart/php

GAE/Gでも、PHPのプロジェクトからindex.htmlだけコピーするだけでも、
プロフィール情報が表示されるようになります。
(適当にAjaxでのリクエストが発生しますが、GAE/G側の実装がなければ404の応答になるだけ)

もうしばらく、いじり倒さないとだめだなと思ったのと、
生産性の低さに嘆く1日でした!

関連するGoogle+の投稿

https://plus.google.com/u/0/114183076079015753160/posts/CYgcSMk3GJN
https://plus.google.com/u/0/114183076079015753160/posts/eSMpESRHTRo

2012年8月2日木曜日

GAE/GでGoogle Cloud Storageを利用するには(1)

いろいろ都合があって、GAE/Gから、Google Cloud Storageを利用してみたので
利用方法をメモしておきます。
GAE/GのFileServiceはなぜか、ドキュメントに記載されていない(8/2現在)ので、
GAE/GのSDKのソースコードを見て使い方を理解する必要があります。
(もしくは、Python版、Java版のプログラムから推測するとか…)
ということで、時間がある時にいじって、Blogにメモしようと思います。

1. Google API Consoleで課金設定をONにする。

Google Cloud Storageは課金設定をONにしないと、書き込みも読み込みも、Bucketの作成さえもできないようなので、課金をONにします。

課金をONにしないとIO_ERRORになってしまいます。

※ローカル環境では、何もしなくても実行可能なので、ローカルでしか動かさないという人は、この手順は不要です。

2. Google APIs ConsoleのTeamにサービスのアカウントを追加する。

GAEのアプリケーションからアクセスさせるためには、API Consoleにサービスのアカウントを追加しなければPERMISSION_DENIEDでエラーになります。

サービスのアカウントの場所は、http://appengine.google.comのGAEのアプリケーションにある、
Application→Application Settingsを開きBasic(一番上)のService Account Nameにあるので、書かれているアカウントをコピーします。

[app-id]@appspot.gserviceaccount.com

みたいなものが、書かれていると思います。
(app-idは作成したappidが書かれています。)

Google APIs ConsoleのTeamのところに、ペーストして、権限設定をして追加すれば完了です。


※ローカル環境では、何もしなくても実行可能なので、ローカルでしか動かさないという人は、この手順は不要です。


3. Go言語でプログラムを作成し、デプロイする。

package controller

import (
 "appengine"
 "appengine/file"
 "fmt"
 "net/http"
)

func init() {
 http.HandleFunc("/", list)
}

func list(w http.ResponseWriter, r *http.Request) {
 c := appengine.NewContext(r)

 name, err := file.DefaultBucketName(c); 
 if err != nil {
  http.Error(w, err.Error(), http.StatusInternalServerError)
 } else {
  fmt.Fprintf(w, "DefaultBucketName = %s", name)
 }

 options := new(file.CreateOptions)
 options.MIMEType = "text/plain"
 options.BucketName = "yourBucketName"

// if wc, absFileName, cErr := file.Create(c, "/gs/yourBucketName/test/sample.txt", options); cErr != nil {
 if wc, absFileName, cErr := file.Create(c, "test/sample.txt", options); cErr != nil {
  fmt.Fprintf(w, "Create Error")
  fmt.Fprintf(w, " %s", absFileName)
  c.Debugf("Create Error %s [%s]", cErr.Error(), absFileName)
 } else {
  fmt.Fprintf(w, "Create Success")
  fmt.Fprintf(w, " %s", absFileName)

  if _, wErr := wc.Write([]byte("Sample Text")); wErr != nil {
   c.Debugf("Write Error = %s", wErr.Error())
  }

  wc.Close()

  if fi, statErr := file.Stat(c, absFileName); statErr != nil {
   c.Debugf("%s", statErr.Error())
  } else {
   c.Debugf("%s %d", fi.Name(), fi.Size())
  }
 }

}

4. 実行

上記のプログラムを実行すると、作成したBucketの中のtestフォルダの中にsample.txtが
作成されていると思います。

GAE/Gだと、file.CreateOptionsにBucketNameが指定されていないと、デフォルトの
BucketNameが設定される様になっているのですが、本番環境では、""(空文字列)なので
作成時にエラーになると思います。(BucketNameは必ず指定する必要があるので)
(ちなみに、ローカル環境だと"app_default_bucket"というのが設定されます)

後は、プログラムを見れば大体わかると思いますが、ファイル作成時は、絶対パス指定でも、
相対パス指定でもファイルの参照は可能です。

絶対パス指定の場合は、先頭に"/gs"をつける必要があります。
書式は「/gs/[Bucket名]/フォルダ名/ファイル名」となります。
フォルダ名を省略すれば、Bucketの直下(という表現が正しいのか?)に作成されると思います。

ということで、課金が恐ろしいですが、
1GBあたり、$0.12とか、$0.01とか、ある程度のところまでは、円高のおかげでうまい棒程度で済みそうなので
いろいろいじってみたいと思います。
(参考:https://developers.google.com/storage/docs/pricingandterms?hl=ja


2012年7月25日水曜日

Go言語のWebフレームワーク"goweb"をGAE/Gで動かす

昨日、公開された(?)Go言語のWebフレームワークの"goweb"を早速GAE/Gで動作するか
試してみました。

導入方法は、以下のように行います。

  1. gowebのソースコードをダウンロードする。
    1. go get code.google.com/p/goweb/goweb
    2. Google Codeからダウンロードする。
    • 1の方法を使った場合は、$GOPATH配下か、GOPATHがなければ、Go言語のコマンドがあるディレクトリにダウンロードされます。
    • ダウンロードを行った場合は、任意のディレクトリに展開して下さい。
  2. ダウンロード後展開 or go getで取得したディレクトリ配下のgowebディレクトリをGAE/Gのアプリケーションディレクトリにコピーする。(GAE/Gの場合は、GOPATH変数を認識しないので、手動でコピーしておく必要があります)
  3. 以下のようなプログラムを作成し、実行する。

package testapp

import (
 "appengine"
 "goweb"
 "fmt"
 "net/http"
)

type PeopleController struct {}
func (p *PeopleController) HandleRequest(c *goweb.Context) {
 context := appengine.NewContext(c.Request)
 context.Debugf("PeopleController#HandleRequest Called.")
 fmt.Fprintf(c.ResponseWriter, "You are looking for person with ID %s", c.PathParams["id"])
 fmt.Fprintf(c.ResponseWriter, "\nServerSoftware = %s", appengine.ServerSoftware())
}

func init() {

 var peopleController *PeopleController = new(PeopleController)
 goweb.Map("/people/{id}", peopleController)

 goweb.ConfigureDefaultFormatters()
 http.Handle("/", goweb.DefaultHttpHandler)
}

gowebパッケージはapp.yamlがあるディレクトリにコピーしておけば良いです。

URLは、http://localhost:8080/people/testみたいな感じにすると、結果が返ってくる事が
わかると思います。

また、appengineのAPIを実行する場合にappengine.Contextを取得したい時は、
HandleRequestメソッドの引数cからappengine.Contextを生成できます。

context := appengine.NewContext(c.Request)

ソースコードもこれから読んでみようと思っていますが、フレームワークを利用せずに
作成するよりはだいぶ楽かもしれないですね。

ちなみに、gowebのディレクトリの中に、goweb_testappディレクトリがあって、その中には
以前のGAE/Gのバージョンで試していた形跡があります。
api_version: 2なので、1.5.3以前かな?


2012年6月5日火曜日

GAE/Gで時間のチェック(Datastore編)

GAE/GのSDK 1.6.4からGo Ver 1が適用され、
時間をデータストアに保存する時は、datastore.Time(int64のtype。今は削除されている)では無く、time.Timeが利用できるようになっています。(参考Blog記事

time.Timeをデータストアに格納しようと思うと、普通に日時が設定されていれば問題ないのですが、
例えば、データストアのプロパティの追加した時など、日時のプロパティに時間が設定されていない時があります。

日時が設定されていないまま、Putを行うと、"time value out of range"のエラーが
発生してしまいます。

これを回避するためには、

  1. PropertyLoadSaverのLoadかSaveで日付をセットする。
  2. リリース前にデータの移行をしておく。(未設定の日時が発生しないようにしておく)
のどちらかになると思います。2のケースだと全件Putされるので問題ないと思いますが、
移行プログラムでデータを移行させない事の方が多いと思います。

したがって、PropertyLoadSaverインターフェイスを実装して、以下のように、
時間がZeroかどうかを判定するロジックを入れておかないと、Putが成功しません。
判定メソッドはGo言語標準で用意されているIsZero()を利用します。
LoadとSaveどちらで設定するのも構わないと思いますが、ここでは、Load時に設定する
方針にしてみます。(if文1つなら処理コストはあまり変わらないだろうと言っても、読み込み時に毎回実行するのは無駄だ。と思われる場合はSaveで実装するのが良いでしょう)

func(hoge *Hoge) Load(c <-chan datastore.Property) error {
        // データストアから読み出し。
 if err := datastore.LoadStruct(hoge, c); err != nil {
  return err
 }

 if hoge.Date.IsZero() {
  // データがない(0000/01/01状態)ものは現在時刻にするなど。
  hoge.Date = time.Now().Format(TimeFormat)
 }

 return nil
}

データが既に入っている場合はIsZero()がfalseになると思われるので、
特に時間の設定は不要(データストアから読みだした時点で代入されている)でしょう。


GAE/GでバッチGetをした時の注意点

最近、Androidアプリ「ええことなかったわー(β)」を開発したのですが、
実は、データを貯めているサーバ側はGAE/Gで開発しています。

本当に、レスポンスとしては早いですよ。
今のところ、リクエストに対するレスポンスは、
ms単位の時間でレスポンスを返しています。
(GAE/Jでも仕事でサービスを利用していますが、レスポンスの内容が全然違いますが、1s前後のオーダだったりするので…)

先ほど、Admin ConsoleでのPerformanceを最弱にしていなかったので、
まだユーザ数も少ないので、最弱(Max Idle Instance = 1+Min Pending Latency = automatic)にしてみました。その結果は後日…ということで。

さて、本題のバッチGetを利用するときの注意点なのですが、
結論から言うと、「指定したキーのエンティティが1件でも無いと、appengine.MultiErrorを返す」ということです。
実際には、以下のようなコードになると思いますが

data := make([]HogeEntity, len(keys))
if err := datastore.GetMulti(c, keys, data); err != nil {
// 何かのエラー
}

全てのキーのエンティティがあれば、エラーの判定にならない(戻り値=nil)なのですが、
1つでもエンティティが存在しない場合、全てエラー(戻り値 = appengine.MultiError)となります。
(おかげで、全部エラーレスポンスに…)

で、これは困ったという事で、以下のようなチェックを行うことになります。

data := make([]HogeEntity, len(keys))
if err := datastore.GetMulti(c, keys, data); err != nil {
 if _, ok := err.(appengine.MultiError); ok {
  // バッチGetだと指定したキーのエンティティが1個でもない場合、MultiErrorが返る。
  c.Infof("Hoge NoSuchEntity")
  return keys, data, nil
 } else {
  c.Errorf("Batch Get Error %s", err.Error())
  return keys, nil, err   
 }
}

実際、appengine.MultiErrorが返されても、存在しているデータは取得できるので
取得したデータが構造体にセットされている部分と、空の部分がsliceに存在する
状態になるので、後で、空の部分を除外するなど、編集処理が必要となります。

ちなみに、appengine.MultiErrorはtype MultiError []errorと定義されていますので
複数のエラーが含まれた状態になることも考慮しておく方が良いかもしれません。
私はGetだけの利用だったので、今のところ、正常なものは利用しています。

MultiErrorの戻り値はGetMulti/PutMulti/DeleteMultiで戻される事があるという
ことなので、(ドキュメントのBasic Operationsに明記)利用するときはappengine.MultiErrorの中身を確認してみてください。

2012年3月29日木曜日

Google App Engine Go SDK 1.6.4を使おうと思ったものの…。

今日の早朝(1:00頃)Go Ver 1のリリースと、
Google App Engine Go SDK 1.6.4がリリースされたので、
変更点をまとめようかと思ったのですが、go1beta4を利用していると
あまり大きな変更点はないので、変更点を洗い出すよりは、開発の手順を
まとめてみようと思います。

とりあえず、ネタを考えるのが面倒だったので
Slim3のチュートリアルと同じ事をGAE/G 1.6.4で実現できるかどうかを
試しつつ進めていこうと思います。

  1. コントローラとテストの作成
事前に、プロジェクトのディレクトリを作成して下さい。
$ cd [任意のディレクトリ]
$ mkdir twitter
$ cd twitter

Slim3のように、build.xmlなんて、便利なものは存在しませんので、手で作成します。

app.yaml
application: twitter
version: 1
runtime: go
api_version: go1

handlers:
- url: /favicon.ico
  static_files: favicon.ico
  upload: favicon.ico

- url: /.*
  script: _go_app

app.yamlの大きな変更点は、api_versionが"go1"になっている点です。
1.6.3までは、"3"で、go1betaは"go1beta"でした。
恐らく、古いバージョンはコンパイルエラーが発生して動かないと思いますので
古いバージョンのプログラムから持ってくる場合はチェックしましょう。

次に、controllerを作ります。
$ mkdir controller
$ cd controller

frontcontroller.goを作成します。このファイルが名前の通り、frontcontrollerとなります。

package controller

import (
 "fmt"
 "net/http"
)

func init() {
 http.HandleFunc("/twitter/", index)
}

func index(w http.ResponseWriter, r *http.Request) {
 htmlText := `
     <!DOCTYPE html>
     <html>
     <head>
     <title>Hello World</title>
     </head>
     <body>
         <p>Hello World</p>
     </body>
     </html>
     `
  
 fmt.Fprintf(w, "%s", htmlText)
}

では、実行します。
開発サーバはdev_appserver.pyを実行するだけなので、twitterディレクトリの1つ上の
ディレクトリで実行すると便利です。

$ python [SDKのディレクトリ]/dev_appserver.py --high_replication twitter

ブラウザで、http://localhost:8080/twitter/にアクセスして、Hello Worldが
表示されれば成功です。
--high_replicationのオプションはHRD環境をエミュレートするオプションです。
現時点でこのオプションを付ける必要は無いのですが、とりあえず入れておきます。

この後、デモでは、フォームの作成で、モデルの処理を作成して、テストコードを
作成してテストを動かしてテストして…と続くのですが、

Go言語標準のコマンド、go testが動作しないため、断念しました。
https://plus.google.com/u/0/114183076079015753160/posts/intFioopvTj

ドキュメントにも載っていないし、恐らく、現時点ではGAE/GのSDKでgo testを利用する事はできないのかもしれません。

ただ、GAE/Gにもtest用のモジュールが存在しているようで、
https://code.google.com/p/gae-go-testing/source/browse/appenginetesting/
これを利用したら良いのではないかと考えている最中です。

17:16:追記
gae-go-testingはだいぶ古そうです。試しにmake installしてみましたが、だめでした。
unit testについてはもう少し様子を見たほうが良さそうです。

もしくは、標準のGo言語のコンパイラをインストールしてGAEのAPIを動かさない
パッケージのみテストを行うという方法を取るしか今は無さそうです。

2012年3月14日水曜日

Slim3のテストを行う時に気をつけること(Datastore編)

開発しているGAEのアプリケーションのデータのメンテナンスを行う必要があるので、
メンテナンス用のコントローラを作成してテストを行おうと思って、テストコードを
書いていました。

テストコードをそのまま掲載するわけにはいかないので、適当なコードを載せます。

public class HogeControllerTest extends ControllerTestCase {
     private HogeService service = new HogeService();

    @Override
    public void setUp() throws Exception {
        super.setUp();

        // データの登録
        Hoge hoge = new Hoge();
        hoge.setId(1);
        hoge.setName("abcd");

        service.put(hoge);
    }

    @Test
    public void run() throws Exception {
        ApiProxy.setEnvironmentForCurrentThread(new UnitTestEnvironment());
        
        tester.start("/hoge");
        HogeController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        
        List<Hoge> list = service.query().asList();
        assertEquals(1, list.size());                     // ここでエラー
    }
}

UnitTestEnvironmentクラスのソースです。
public class UnitTestEnvironment implements Environment {

    @Override
    public String getAppId() {
        return "Unit Tests";                // setupを使う場合はこれにしておかないとquery()でデータが取れない。
    }

    @Override
    public Map<string, object> getAttributes() {
        Map<string, object> map = new HashMap<string, object>();
        map.put("com.google.appengine.server_url_key", "dummy");
        return map;
    }

    @Override
    public String getAuthDomain() {
        return "gmail.com";
    }

    @Override
    public String getEmail() {
        return "admin@gmail.com";
    }

    @Override
    public String getRequestNamespace() {
        return "";
    }

    @Override
    public String getVersionId() {
        return "unittest";
    }

    @Override
    public boolean isAdmin() {
        return true;
    }

    @Override
    public boolean isLoggedIn() {
        return true;
    }

}

上記のUnitTestEnvironmentだとテストは正常終了します。
これは、setupでのデータストアの書き込み時のapplicationIdと
テスト実行時の呼び出しの時のapplicationIdが一致するからです。

失敗していた時というのが、TaskQueueのテストをしようとして参考にした
UnitTestEnvironmentのapplicationIdが"MyApplicationId"とかにしていたので
setUpの時とapplicationIDが異なってしまっていました。
その影響で、何度queryを実行してもデータが1件も取れないという状況に
陥っていました。

なので、setupを使う時というか、データストアを使ったテストを行う場合は
以下の点に注意した方が良さそうです。

  1. appidをデータ登録時と参照時で合わせておく。
  2. Slim3のTestEnvironment.javaで設定されているappIdは"Unit Tests"

2.はsetupを使わず、自分でデータを登録する場合は問題ないと思いますが、
初期データなど使う場合や、事前にlocal_db.binを用意する場合などは
気をつけておかないとデータが取れない現象に遭遇するかもしれません。

以下、参考までに当時の私のG+に投稿したコメントです。

TaskQueueに入っている事を確認するテストで苦戦している様子(Google+)
https://plus.google.com/u/0/114183076079015753160/posts/NUceVDbNKDK

データストアからフィルタなしのqueryでデータが取れなくて苦戦している様子(Google+)
https://plus.google.com/u/0/114183076079015753160/posts/MZhuGtjrwTS

Slim3 Source Code Reading #12

今日は、Slim3 Source Code Reading #12でした。

今回の内容はRouterを読み進めました。ソースファイルは以下の4ファイルです。

  • Router.java
  • RouterFactory.java
  • RouterImpl.java
  • Routing.java

少し前に、私が仕事で開発しているGAE/JのアプリケーションでRouterを導入したので
ソースコードを事前に読んでいました。
(Googleで検索せずにFrontController.javaを読み進めて実装方法を解析したのは、ソースコードリーディングをした影響でしょうか。)
また、@sinmetalさんと2人だったので、ざっと読んだ感じになりました。

最初にRouter.javaですが、Routerクラスはインターフェイスで、
静的ファイルの判定と、リクエストURLを変換した後のURL文字列を返すメソッドの2つがあります。

次に、RouterFactory.javaですが、これはFrontControllerからRouterを呼び出すために
インスタンスを生成するクラスです。
getRouter()は定義したRouterか、デフォルトのRouterを取得します。
ここで取得したRouterのインスタンスはservletContextに格納され、2回目以降は、キャッシュされたインスタンスが取得されます。
初回は、Routerのインスタンスを生成しますが、そのメソッドがcreateRouterメソッドです。
この中身を見れば分かるのですが、自前のRouterはRootPackage.controllerPackage.AppRouterクラス固定となっています。
配置する場所も、名前も間違っていると反映されないことになります。

RouterImplクラスは、Slim3を使う上ではデフォルトのRoutingの処理が書かれています。
仕組みは、ListにRoutingのルールを追加しておいて、Listの中に対象のURLがあるかどうかを判定するようになっています。URLがあれば、変換後のURL文字列を返します。
静的ファイルの判定(isStaticメソッド)は以下の様になっています。

  • /_ah/で始まらない。(管理画面などのURL)
  • 最後が拡張子ではない(.jpgなどで終わらない)か、拡張子が"s3"で始まらない

拡張子が"s3"で始まるファイルは静的ファイルではないと判定するようです。
そんなファイルがあるかな?と考えてみましたが、その時は思いつきませんでした。
クラスにはS3で始まるクラスもあるようですが、”拡張子”がs3で始まるファイル…。

最後にRoutingクラスですが、このクラスは、変換前のURL文字列と変換後のURL文字列を持ちます。
ざっと説明すると、"/hoge/fuga/{param}"を"/hoge/fuga?param={param}"のように
変換するという処理を行います。
fromの{}で囲まれた部分を、toの{}で囲まれた部分に配置するという感じです。

複数の{}を指定する事も可能です。

"*"を指定することも可能なのですが、今回はこの"*"の扱いにはまりました。
読書会での結論は"*"を使うと、それ以降は全てひとつのパラメータに格納され、
URLがエンコードされるようになるということでした。
また、{}の中に"*"を使うことはできません。

最後に、前回のMockXXX.javaもそうだったのですが、Router関連も
先頭にコメントがあって、CopyRightはSeasar Foundationになっていました。
Apache Licenceが適用されるようです。
(単純にSeasarからコピーしたということなのでしょうか?)

今回でひとまず、Slim3 Source Code Readingは最終回となりました。
振り返り、まとめの記事は後で、別途残そうと思いますが、
約3カ月、全12回の長い活動になりました。
最初から最後まで読み進めた結果、ある程度はSlim3でGAE/Jの
アプリケーションが作れるようになったような気がします。

今後、GAE/JのSDKのバージョンアップに合わせて
Slim3のバージョンアップも行われると思いますので、
新しくなった時に、また個人的にか、イベントとしてやるかは、
わかりませんが、再度読み進めてみたいと思います。


2012年3月7日水曜日

Slim3 Source Code Reading #11

昨日、Slim3 Source Code Reading #11でした。

今回もtesterパッケージを読みました。
今回はMockXXX.javaとHeaderUtil.java、UrlFetchHandler.javaでした。

MockXXX.javaに関しては、リクエスト、レスポンスを擬似的に再現するための
もので、中身もgetter/setterばかりで特にこれといったものはありませんでした。

ただし、MockHttpServletRequestに関してはHeaderなども変更できるようなので、
例えばjsonのリクエストをテストする場合は事前にHeaderを書き換えた状態で
テストを行うことも可能な感じがしました。
後は、cron jobの実行だとヘッダに”X-AppEngine-Cron: true”が付く事に
なっていますから、手動で実行した時は無視する処理をいれている場合は
Controllerの実行前にHeaderを追加してからテストするという事も
可能だと思います。

HeaderUtilも時刻のフォーマットが定義されているだけで、これも直接利用する事は
無さそうです。

UrlFetchHandlerはinterfaceで、中身を自分で実装する必要があります。
Slim3Demoにもこのクラスを使ったDemoは存在していないようなので、
そもそも利用していないのか?という話になりました。

本題に移ると、これは実際に動作の確認はしていませんが、
アプリケーションでGoogle App EngineのURL Fetch APIを利用した時の
外部サイトのテストで使うために利用するようです。
interfaceを実装する形になりますから、テストデータは自由に作ることができます。

恐らく、以下のようなテストコードになると思います。※以下のコードは未検証です。

@Test
public void URLFetchAPIを使うコントローラのテスト() throws Exception {
    tester.setUrlFetchHandler(new HogeUrlFetchHandler());

    tester.start("/urlfetchsample/");
    AddController controller = tester.getController();
    assertThat(controller, is(notNullValue()));
    assertThat(tester.isRedirect(), is(true));
    assertThat(tester.getDestinationPath(), is("/"));
}

URL Fetch API用のHandlerオブジェクト ※以下のコードは未検証です。
public class HogeUrlFetchHandler() implements UrlFetchHandler {
    public byte[] getContent(URLFetchRequest request) throws IOException {
        String strResponse = "何かのデータ";

        return strResponse.getBytes("UTF-8");
    }

    public int getStatusCode(URLFetchRequest request) throws IOException {
         return HttpServletResponse.SC_OK;
    }
}

昨日の段階では「これを利用する事はあるかな?」という話になりましたが、
既に動作中のWebAPIでテストを行う場合は、URLFetchHandlerは
不要なのかもしれませんが、
動作中のAPIだと実行時点でデータが変わったりするのでテストの結果が
実行するたびに変わってしまうという結果に陥りそうなので
作るべきかな?と思ったりしています。

あとは、WebAPIも作成途中でテストデータが無いと言った時も利用することに
なるでしょう。

今回は中身も大した内容ではなかったので、テストコードをどれくらい書くか?
みたいな話題になりました。
(読書会での結論は「不安な所をテストする」と言われているので、「不安がなくなるまで書け」ば良いようです。)

さて、次回(#12)はorg.slim3.controller.routerを読んで、最終回となります。
ご興味がありましたら、ご参加下さい。

2012年2月29日水曜日

Slim3 Source Code Reading #10

今日はSlim3 Source Code Reading #10でした。

前回に引き続き、testerを読み進めました。
今日は、以下のファイルを読みました。

  • ControllerTestCase.java
  • ControllerTester.java
  • ServletTester.java

ControllerTestCase.javaはAppEngineTestCaseと同じように中身は
testerというメンバ変数があるだけでした。ので、スルー。

testerはControllerTesterクラスの変数なので、そのまま、ControllerTester.javaに
進みました。

ControllerTesterはsetUpで、初期化パラメータの読み込みと、
FrontControllerの初期化を行っています。
これが、リクエストを受け取った時のFilterの初期化の代わりとなっているようです。

tearDownは後処理が行われて終わりです。

startメソッドが、実際にControllerを実行するメソッドですが、内部でRouterも
読み取るので、Routerの設定は反映されます。

以下の3メソッドはControllerに合わせてあるのだと思いますが、
中身も少ないので、概要だけ。
getDestinationPathは遷移先のパスを返します。
getControllerはrequestScopeに格納されているControllerを返します。
getErrorsはValidationエラーの情報を取得します。

最後にServletTester.javaですが、
setUpでは、元の設定を退避し、本番環境に合わせるように設定されるようです。
例えば、タイムゾーンは"UTC"をセットしていますし、ロケールはLocale.USがセットされます。

tearDownで、退避していた元の設定を復元しています。

param()はパラメータの取得/設定します。このメソッドを使ってテスト前の
パラメータを指定する事ができます。
paramValues()はパラメータを配列で取得/配列を設定します。

以下のメソッドは型変換を行った後の値を返すメソッドです。
Controllerクラスにもありました。パラメータの型もチェックしたい場合は
利用した方が良いでしょう。
asShort()
asInteger()
asLong()
asFloat()
asDouble()
asString()
asBoolean()
asDate()
asKey()

以下のメソッドは、それぞれのスコープの値を取得/設定します。
requestScope()
sessionScope()
applicationScope()

isRedirect()はリダイレクト先のパスが設定されていればTrueを返します。

今日は、薄い内容で終わるかと思ったのですが、最後の

addBlobKey()でハマりました。
これは、Blobstoreのテストを行うために、実行前にBlobstoreのKeyを設定
しておくものだと思うのですが、どうしても使い方がわからなかったので
slim3Demoのソースコードを見ました。

使い方としては、第1引数にブラウザからのパラメータ名を指定し、
第2引数には恐らく、BlobKeyの文字列を渡すのだと思っているのですが、
どうしてもテストがうまくいかない。
JUnitのトレースを見ると、以下のような表示になりました。
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List
at com.google.appengine.api.blobstore.BlobstoreServiceImpl.getUploads(BlobstoreServiceImpl.java:162)
at com.google.appengine.api.blobstore.BlobstoreServiceImpl.getUploadedBlobs(BlobstoreServiceImpl.java:135)
at slim3.demo.controller.blobstore.UploadController.run(UploadController.java:21)
at org.slim3.controller.Controller.runBare(Controller.java:111)
at org.slim3.controller.FrontController.processController(FrontController.java:491)
at org.slim3.controller.FrontController.doFilter(FrontController.java:277)
at org.slim3.controller.FrontController.doFilter(FrontController.java:237)
at org.slim3.controller.FrontController.doFilter(FrontController.java:199)
at org.slim3.tester.ControllerTester.start(ControllerTester.java:131)
at slim3.demo.controller.blobstore.UploadControllerTest.run(UploadControllerTest.java:20)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
どうやら、キャストで失敗しているようで、バグなのでは?という結論で、
読書会としてはタイムアップ。

その後、Twitterで、「slim3DemoがSDK1.6.0だと動作した」という報告が
あったので、私も同様にslim3Demoをダウンロードした後、実行してみると
同じ例外で失敗する結果となりました。

私の環境はSDK1.6.2.1なので、何か変わったのかと思ってSDKのソースをみると、
確かに変わっていました。

以下のリンクは、SDKのソースコードのDiffです。

元々は、Map<String, String>で扱っていたようなのですが、
Map<String, List<String>>に変わってしまったようで、
最新のSDKだとaddBlobKeyを使っても
テストは動作しないという結果になってしまいました。

現時点では、Slim3の更新を待つか、
requestScopeに以下のKeyで型をあわせてセットすれば動くかもしれません。
com.google.appengine.api.blobstore.upload.blobkeys

という事で、次回もtester(MockXXX)から読み進めます。

次回は3/6(火) 19:00〜を予定しています。ご興味がありましたらどうぞ。



2012年2月22日水曜日

Slim3 Source Code Reading #9

今日はSlim3 Source Code Reading #9でした。

今回は読むのを忘れていたModelRefとAppEngineTestCase、AppEngineTesterの
辺りを読みました。

最初にModelRef.javaについて。
読み方は上から順番に読む感じでした。
まず、ModelRef.javaで気にしたのが、getModel()です。
例えば、Slim3本から引用すると、以下のような記述になります。
person.getAddressRef().getMode();
このgetModel()が、Memcacheから取得したインスタンスの場合
どうなるかが気になっていたからです。(課金的な面で。)

結果としては、Memcacheに入れてもModelRefに関してはデータストアへのオペレーション回数は減りません。以下のようにトランザクションがない、getを実行するからです。

model = Datastore.getWithoutTx(getModelMeta(), key);

ただし、これは初回だけで、2回目以降の呼び出しではメンバ変数を参照するため、getは実行されません。

また、getAddressRef()は、modelを更新する時にmodelMeta#modelToEntity()メソッド(これは自動生成される)を介してデータストアに保存されますが、データストアにはm.getAddressRef().getKey()で取れるKeyが格納されます。

次に、InverseModelRef.javaについて
これも上から順番に読み進めたのですが、ModelRef.javaと大体同じだったのですが、
getModel()が以下のようになっていました。

        model =
            Datastore.query(getModelMeta()).filter(
                mappedPropertyName,
                FilterOperator.EQUAL,
                key).asSingle();

これは、参照先のModelにプロパティを付加して、それを条件にQueryでデータを取得しています。ただ、1件ならKeyで取れば良いのに…という話になりましたが、よくわからなかったので、ひとまずスルー。

最後に、InverseModelListRef.javaについて
これも上から順番に読み進めました。このクラスは上記2つと違って、内部で、ModelQueryクラスのインスタンスを生成し、modelListを取得します。
複数のデータを取得するので、Filter、Sortも使えます。In-memoryのFilterとSortも使えます。

InverseModelRef、InverseModelListRefの両方とも、指定したプロパティ名のプロパティに格納されているKeyを取得するようになっています。

        public List getModelList() {
            Key key = getOwnerKey();
            if (key == null) {
                modelList = new ArrayList();
            } else {
                query.filter(mappedPropertyName, FilterOperator.EQUAL, key);
                if (!sortsSet) {
                    query.sort(defaultSorts);
                }
                modelList = query.asList();
            }
            return modelList;
        }

最後に、ControllerTestCaseか、AppEngineTestCaseをどちらを先に読むかを迷ったのですが、AppEngineTestCaseから読むことにしました。一般的に先にModelとServiceの作成、テストをするような気がしたので。

AppEngineTestCase.javaは、内部にAppEngineTesterのメンバ変数をもつ、
一般的なJUnitのメソッド(setUp、tearDown)があるだけでした。

したがって、AppEngineTesterを引き続き読み進めました。

AppEngineTesterは、色々ありました。

最初にstaticの初期化がありましたが、AppEngineUtil.isServer()の動きがよくわからなかったので、いろいろテストコードを書いて動かしつつ理解を深めました。
AppEngineUtil.isServer()については、SystemPropertyの"com.google.appengine.runtime.environment"がnullでなければtrueを返します。
これは、開発サーバだと"Development"、本番環境だと"Production"を返します。
(※詳細はドキュメント参照)
しかし、JUnitTestだとnullになります。(たしかに、JUnitTestはサーバではない。)
appengine-web.xmlに記述してみましたが、この値に関しては書き換えることができませんでした。(確認したのはJUnitTestでの実行のみです)
したがって、JUnitTestの時はサーバの環境が取り込まれていないので、初期化時に
必要なライブラリを取り込んでいます。
取り込んでいるのはApiProxyLocalImplとLocalDatastoreService、LocalServerEnvironmentのようです。

setUpでは、ApiProxyのEnvironmentとDelegateを退避しています。これは、恐らく、テストごとに初期化するためだと思われます。
メンバ変数のEnvironmentとDelegateはtearDownで元に戻されます。

tearDownでは、GlobalTransactionのロールバック、アクティブなトランザクションのロールバック、Memcacheのクリア、データストアに書き込みを行ったKeyのエンティティの削除、メールの削除が行われています。

ここで、最初に議論になったのが、「Commitしてしまうと削除されないんじゃないのか?」と
言う話をしました。パッと見た感じだと、deleteKeyを取得している部分がないからです。
が、結論を先に言っておくと、deleteKeyはDatastore#putを呼び出し、実行した時点で
登録されます。


これは、457行目の

ApiProxy.setDelegate(this);

で、自分のインスタンスをApiProxyのDelegateに登録しています。
これを行うことで、APIが実行されるたびに、Delegate<Environment>#makeSyncCall()が呼び出されるようになります。
Delegateはインターフェイスなので実装したクラスである必要がありますが、
AppEngineTesterはDelegateインターフェイスを実装しているので、
515行目のmakeSyncCall、599行目のmakeAsyncCallが呼び出される事になります。
あとは、中身を見ていくと、Service名で条件分岐してそれぞれのAPIに対する処理が
行われています。
Datastoreに関してはmakeSyncCall内でKeyを全て拾っているということになります。

それから、もう一つ重要だったのが、AppEngineTesterのenvironment変数です。
これは、ユーザの情報を持っていて、JUnitTestでUserServiceを利用する時に
重要です。
environment#setEmail()を使ってメールアドレスを設定しておくと、UserService.isUserLoggedIn()がTrueになり、認証済みの状態を作る事ができます。
注意する必要があるのは、emailを空にして、setAdmin(true)にすると、
”ログインしていないが、管理者権限がある”状態が作られてしまいますので、気をつけてください。
これを使うことで、Controllerのテストも可能になります。
同様にFederated Loginの情報も事前にセットしておけば、同じくテストすることが
できるでしょう。(※ただし、自分でセットすることになるので事前にFederated Loginした時の
環境は調査しておく必要があります)
他にもattributeにkeyと値をセットすれば同様に環境を作ることができます。

以下が、環境の動作確認で利用したテストコードです。
public class SampleTest extends AppEngineTestCase {
    @Test
    public void サーバチェック() {
        assertFalse(AppEngineUtil.isServer());
        assertNull(SystemProperty.environment.value());
    }
    
    @Test
    public void ユーザ認証テスト() {
        tester.environment.setEmail("test@example.com");
        tester.environment.setAdmin(true);
        
        UserService user = UserServiceFactory.getUserService();
        assertTrue(user.isUserLoggedIn());
        assertEquals("test@example.com", user.getCurrentUser().getEmail());
        assertTrue(user.isUserAdmin());
    }
}

他にも色々attributeにセットすると面白い事ができそうですが、
今日はここまでにしておきます。

次回は、2/28(火)です。続きでtesterを読み進める予定です。
https://sites.google.com/site/chugokugtug/sourcereading/slim3/10
ご興味がありましたらどうぞ。

2012年2月15日水曜日

Slim3 Source Code Reading #8

Slim3 Source Code Reading #8を開催しました。

先に言っておくと…
今回はJSPを読むのみで終わってしまいました。Seasarを使っている人には「ほぼ常識」と言わんばかりの内容しかないと思いますが、よろしければ読んで頂ければと思います。

ちなみに、私に取っては新発見のものもあって、適用しようと思っています。

jspで記述する場合はf:h()など"f:"を付けて呼び出します。

h()
h()は引数がString型の場合は、エスケープして出力します。Keyの場合はKeyFactory.keyToString()の結果が出力されます。
String、Key以外の型の場合は引数.toString()が実行されます。
したがって、エスケープの必要がある場合は、必ずh()を使いましょう。

url()
引数に":"が含まれる場合はそのまま出力します。(恐らく、URLが書かれていると判断する)
ここでハマったのですが、中でrequest.getContextPath();を取得しています。
これはコンテキストパスを取得するのですが、GAEだとコンテキストルートの設定が
ないので、必ず空になるのでは?という疑問がでました。
実際、開発環境でTwitterサンプルを動かして、中身を見てみたのですが、やはり空でした。
必ず空になるなら無視しても良いような気もするのですが、疑問が残ったままとなりました。
処理としては、":"が付いていればそのまま返す(URLが書かれている)
引数が空の場合、
requestの中に"slim3.basePath"が含まれていれば、その値を、
含まれていなければ、サーブレットパスを取得し、
コンテキストパス+取得した値を連結して返す。

引数の先頭が"/"で始まっている場合、
コンテキストパス+引数を連結して返す。

空でもなく、先頭が"/"で始まらない場合、
コンテキストパス+"slim3.basePath" or サーブレットパス+引数の値を連結して返す。
返すURLはエンコードされたURLになる。

blobstoreUrl()

BlobstoreServiceからBlobStoreへのアップロードURLとリダイレクト先URLを設定した
URLを返す。リダイレクト先URLの生成ルールはurl()と同じ。
※このメソッドではコンテキストパスを取得していない。

nbsp()
空白を&nbsp;に置き換える。

br()
改行を<br />に置き換える。¥r¥n→¥n→¥rの優先順位で置き換える。
エスケープと改行、空白の変換を全て適用したい場合は、
f:br(f:nbsp(f:h(contents)))としないといけない。brとnbspが入れ替わるとおかしくなる。
f:hを最後にしても<br>がでてしまうのでNG。

locale():ロケールを取得
timeZone():タイムゾーンを取得

text()
引数で指定したnameに対する値をrequestから取得し、name="key" value="value"という文字列を出力する。valueの方は値がエスケープされる。inputタグなどで利用すると便利
※XXXArrayという名前だと例外が発生する。

hidden()
中身はtext()

checkbox()
checkboxで利用する。
引数で指定した名前がrequestに存在すればchecked属性が付加される。
※XXXArrayという名前だと例外が発生する。


multibox()
複数のcheckboxで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればchecked属性が付加される。requestのにはListを入れておかなければならない。Listでなければ例外が発生する。
※XXXArrayという名前でなければ例外が発生する。

radio()
radiobuttonで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればchecked属性が付加される。
※XXXArrayという名前だと例外が発生する。

select()
optionタグで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればselected属性が付加される。
※XXXArrayという名前だと例外が発生する。

multiselect()
複数選択時のoptionタグで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればselected属性が付加される。
※XXXArrayという名前でなければ例外が発生する。

errorClass()
引数で指定した名前のvalidationのエラーがあれば、
引数で指定したCSSクラス名を設定する。
エラーがなければ空を返す。

errors()
validationのエラーなどを出力する。検証エラー全てが表示される。

key()とhiddenkey()はDeprecatedとなっています。
それぞれ、h()かhidden()を使いましょう。

request()
protectedのメソッドなので直接利用する事はありませんが、
HttpServletRequestオブジェクトを取得しています。

後は、JavaScriptの話題になりましたが、私もまだまだJavaScriptのスキルが足りないようなので、微妙な感じで終了しました。

私のWebの技術が普通の人レベルになるにはまだまだ遠いようです。

次回のSlim3 Source Code Reading #9は2/21(火)です。
ModelRefとtesterを読み進める予定です。
ご興味がありましたらどうぞ。



2012年2月7日火曜日

Slim3 Source Code Reading #7

今日はSlim3 Source Code Reading #7でした。

今日の範囲はValidatorの辺りを読みました。
Validatorは入力チェックを行うためのクラスで、以下の型のチェックと、
範囲チェック、長さチェック、必須チェック、正規表現チェックができるようになっています。

  • byte(Validators#byteType())
  • short(Validators#shortType())
  • int(Validators#integerType())
  • long(Validators#longType())
  • float(Validators#floatType())
  • double(Validators#doubleType())
  • Number(Validators#numberType())
  • Date(Validators#dateType())
型チェックはそれぞれ、Byte.valueOf()等を呼び出し、例外がスローされるかどうかで
判定されます。ただし、NumberとDateはそれぞれ、DecimalFormatとSimpleDateFormatクラスでparseした結果、例外がスローされなければOKとなります。

実は、dateType()に関しては気を付けないといけないケースがあります。
以下のテストコードで確認したのですが、日付が不正なケースでもOKと判定される事があります。

    @Test
    public void 日付の確認() {
        input.put("arg1", "2012-02-07");
        input.put("arg2", "2012-02-30");
        input.put("arg3", "2012-02-1a");
        input.put("arg4", "2012-02-0a");
        input.put("arg5", "2012-02-01");
        input.put("arg6", "2012-02-29");
        
        Validators v = new Validators(input);
        v.add("arg1", v.dateType("yyyy-MM-dd"));
        
        assertTrue(v.validate());
        
        // 存在しない日付
        try {
            Validators v2 = new Validators(input);
            v2.add("arg2", v2.dateType("yyyy-MM-dd"));
            
            assertFalse(v2.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付が不正(2012-02-1aは正しいと判定される)
        try {
            Validators v3 = new Validators(input);
            v3.add("arg3", v3.dateType("yyyy-MM-dd"));
            
            assertTrue(v3.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付が不正(その2)(2012-02-0aは間違いと判定される)
        try {
            Validators v4 = new Validators(input);
            v4.add("arg4", v4.dateType("yyyy-MM-dd"));
            
            assertFalse(v4.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付チェック(正しくはこう)
        try {
            Validators v5 = new Validators(input);
            v5.add("arg3", v5.dateType("yyyy-MM-dd"), v5.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
            
            assertFalse(v5.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        Validators v6 = new Validators(input);
        v6.add("arg5", v6.dateType("yyyy-MM-dd"), v6.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
        
        assertTrue(v6.validate());

        Validators v7 = new Validators(input);
        v7.add("arg6", v7.dateType("yyyy-MM-dd"), v7.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
        
        assertTrue(v7.validate());

    }

arg3の"2012-02-1a"という文字列はSimpleDateFormatで変換すると、日付は正しくありませんが、例外がスローされず、日付のインスタンスが返されます。
従って、本来は正しくない日付なのに、正しい日付という判定結果が返ることになります。
したがって、後半のチェックのように、正規表現でのフォーマットチェックも行う必要があります。
※これは両方必要です。正規表現のみだと、arg2のような、存在しない日付を通してしまうからです。

範囲チェックは、Validators#longRangeとValidators#doubleRangeがあります。(小さい範囲の型も同様にこれでチェックすることになると思います)

使い方は、v.longRange(3,10);と言う感じになります。
これだと、3以上、10以下(3と10を含む範囲)がOKとなり、それ以外の値の場合はNGとなります。doubleRangeに関しても使い方は同じです。

必須チェックはValidators#required()を指定する事でチェックがかかります。
nullか""(空文字)の場合、NGとなります。

長さチェックは、Validators#maxlength()、minlength()を利用します。
これは、文字数(バイト数ではない)をカウントして、引数で指定した文字数を超える(maxlength()の場合)か、下回る(minlength()の場合)かすると、NGとなります。
カウントの方法は、String#length()の値を比較しています。

Validationについてはこんな感じで、大した量ではありませんでしたが、ここから、使い方の話題に移りました。

これも今日見つけたのですが、ControllerクラスにasInteger()など、それぞれの型でrequestパラメータを取得する便利なメソッド達があります。
本来の使い方としては、Validatorでチェックをした後、asXXX()を使って、
変数に取り込む使い方が正しい使い方だと思います。
※nullを許容する場合はnullの扱いについても考慮は必要なので、プリミティブ型ではなく、クラスの変数に代入しましょう。
request.getAttribute("param").toString()をわざわざ変換したり、requestScope("param")をわざわざ変換するのは、カッコ悪いという事でした。
(※私のプログラムを見ると、混ざった状態でひどい事が判明したのでリファクタリングします)

asXXX()には、asKey()とasMap()もあります。asKey()はKeyFactory#keyToString()の結果をKeyに変換し、asMap()はHttpServletRequestをRequestMapに変換してくれます。

その後、少しjspのFunctionを見ていたのですが、そこで、blobstoreUrl()を見つけました。
前回の#6の時にBlobstoreのサンプルを見せたのですが、最初のBlobstoreに格納するURLを取得する部分がjsp側でf:blobstoreUrl(リダイレクト先URL)と書くだけで実現
できそうです。

それから、このBlogの記事を+1してくれていた、shin1ogawaさんのGoogle+のコメントがある事に気がついて(今日言われて気がついた)、FileServiceとか、Google Cloud Storageなんかの話題も触れる機会になりました。

それから、Blobstoreの最大ファイルサイズですが、前回32Mと言っていたのですが、
Goの場合(http://goo.gl/1lvU2)だけで、Python/Javaだと2GBまでいけそう(http://goo.gl/mX5Dy)です。

また、私が作ったGAEのアプリケーションで妙なリダイレクト(302のレスポンスを返す)が発生しているケースがあったので、聞いてみたのですが、
どうやら、リクエストのURLの最後の"/"が怪しいようでした。
ログを詳細に分析すると、IndexControllerを実行するURLで、最後に"/"があると、
200を返すのに、"/"がない場合は302を返していました。
IndexControllerでなければ、そういう現象は見受けられませんでした。

したがって、IndexControllerを使う場合は、URLの最後に必ず"/"を入れておかないと不具合の元になりそうな予感がしました。
特に、requestScope的な部分で。

次回は、jspをもう少し見るのと、ModelRef、testerと順番に読んでいく予定です。

2012年2月3日金曜日

Google App Engine Go SDK(Go1ベータ版搭載版)が公開されています

Go1betaが搭載されたGoogle App Engine SDKが公開されています。
http://code.google.com/p/appengine-go/downloads/list

先日公開されたGoogle App Engine SDK 1.6.2はr60.3という
ちょっと古めのバージョンのままでしたが、
もうすぐGo1がリリースされるのと、最新のGo言語と書き方も違うというのも
開発者からすると大変ですので、ここで、どのような違いがあるかを
まとめておきたいと思います。
(※恐らく、正式版でも大きな変更はないと思いますが、あくまでもGo1ベータでの
違いの確認です。また、ベースは私の書籍のプログラムをベースに見ていきます。)

なんとなくですが、近いうちに公式リリースもされそうな気がするので、
先にこのGo1betaで慣れておく方が良いかもしれません。

最新ドキュメントのURL
http://weekly.golang.org/

私の書籍「Go言語プログラミング入門 」でもリリース後は
読み替える必要がありますのでご注意下さい。

  • app.yamlのapi_versionは"3"ではなく、"go1beta"となります。
    • ※これに関しては、正式なリリースになれば変更されるでしょう。
  • httpパッケージ→"net/http"に変更
    • 通信関係のパッケージは大体netパッケージ以下に移動しています。したがって、これまでは、import "http"でしたが、import "net/http"となります。
  • os.Error→"error"に変更
    • Errorは、osパッケージで定義されたインターフェイスでしたが、パッケージ内のインターフェイスではなく、Go言語全体(?)で使えるインターフェイスになりました。また、エラーメッセージを取得するメソッド名がString()からError()に変わっています。
  • time.SecondsToLocalTime()がtime.Unix()に変更
    • これまで、ローカル時間を取得するメソッドはSecondsToLocalTime()でしたが、Unix()に変わっています。Unix()メソッドは、第1引数が秒までを指定し、第2引数でナノ秒(小さい時間)を指定するようになっています。
  • time.Seconds()がtime.Now()に変更
    • 現在日時を取得するメソッドが変更されています。Seconds()はメソッドになりました。秒を取得します。また、time構造体の中身が秒、ナノ秒、Location位置の3つで管理されるようになっています。
  • templateパッケージがtext/templateに変更
    • 使い方は変わっていませんが、templateパッケージが移動しているようです。
  • template.ParseFile()が、template.ParseFiles()に変更(2012/02/03追記)
    • template.ParseFile()でファイルを1つのみ解析していましたが、引数に複数のファイル名を渡す事で、複数のファイルを解析できるようになっています。これまで通り、1つでも実行できますので、メソッド名のみ変更しておけば問題ありません。
  • template.Must(template.New("html").ParseFiles("sample.html"))→template.Must(template.ParseFiles("sample.html"))にする。
    • template.ParseFiles()がtemplate構造体のメソッドと関数の2つに増えています。単純にhtmlを読み込んで実行するの(書籍の例など)であればtemplate.ParseFiles("sample.html")など、New()を呼び出す必要はありません。
  • datastore.Timeが削除されています。go1beta以降では、time.Timeを利用すれば良いようです。
    • フォーマットを指定したい場合は、Format()が用意されていますので、書籍では、fmt.Sprintfを使って書式指定していましたが、go1betaでは、comments_view[pos].Date = comment.Date.Format("2006/01/02 15:04:05")という感じになります。
    • 以前書いたBlog記事(Goでの日付変換)も参考にして下さい。
  • datastore.SecondsToLocalTime()も削除されています。
    • datastore.Timeで管理する必要がなくなりますので、time.Timeで定義し、time.Now()で取得して下さい。


2012年2月1日水曜日

Slim3 Source Code Reading #6

今日はSlim3 Source Code Reading #6でした。

今日は、FileUploadの辺りを読む予定でしたが、
最近Slim3で開発を始めたので、それに対する疑問を聞いてみたり、BlobstoreAPIの
使い方を説明したりしました。

あとは、最近Google App EngineのアプリケーションをSlim3を使って
開発しているので、その時に疑問に思ったことを聞いてみたりしました。
先週と同じく、言いだしっぺの2人だったので、すぐ脱線してしまいました。

ちゃんと、FileUploadの部分も読みました。

が、HTTPのリクエストに対する知識がないと、意味不明な部分もあったりして、
Chromeのデベロッパーツールでhttp headerの中身をみたりしました。
個人的には十分な内容だったと思います。

では、一つ目のBlobstoreAPIを利用したファイルのアップロードについては、
以下の手順でファイルをBlobstoreに格納します。

  1. アプリケーションのファイル選択画面を表示する。
    1. ここで、POST先のURLはBlobstoreService#createUploadURL(url)を呼び出して、BlobstoreAPIで利用するURLを指定する。
    2. 引数のURLはBlobstoreにデータを格納後にリクエストされるURLを指定する。
  2. アップロード後に呼び出されるURLで、BlobstoreService#getUploadsを呼び出し、Blobstoreに格納したデータのBlobkeyを取得する。(複数ファイルに対応しているため、戻り値がMap<String, List<BlobKey>>の型になっている。)
    1. この時取得したBlobKeyは忘れてはいけない。(BlobKeyで格納しているデータにアクセスするため、BlobKeyを参照できないとデータを参照する事も、削除することもできなくなる。※ただし、admin consoleからなら削除可能)
  3. BlobKeyを引数にBlobstoreService#serve()を呼び出すことで、HttpResponseにBlobstoreに格納されているデータを書きこんでくれる。
    1. 画像であれば、<img>タグのsrcに指定することで、画像が表示される。
    2. 動画であれば、<video>タグのsrc
    3. 音声であれば、<audio>タグのsrc
  4. 同じく、Blobkeyを引数にBlobstoreService#delete()を呼び出すことで、Blobstoreのデータを削除する。ただし、Entityに格納している場合は、Entityの中身(BlobKey)を読みだした後に削除しないと、BlobKeyが参照できないのでdeleteできなくなる。
ちなみに、私のGoの本を参考にサンプルプログラムを作りましたが、(私だけかもしれませんが)意外と作れます。(Go→Javaができたという事は、その逆もまた可能なはず)

BlobstoreAPIはファイルサイズが最大32Mバイトまでとなっていますので、
大きなデータは格納できません。

脱線した話として、

  1. 自作のControllerに親クラスを作ったりするかどうか。
    1. これはやる。Sessionなど、全体に渡って取得する場合はやる。
  2. データストアから読みだした後、表示用に編集した値を保持するクラスをどこに作るか。
    1. 特にルールはないので、どこでもいいんじゃないか。
    2. Modelで編集用のデータを返すメソッドを定義する
  3. Keyのnameに名前を格納しておくと結構いい感じ。
    1. エンティティを読み出すのとKeyのみ読み出すのとでは、コスト的に7分の1
  4. とりあえず、良く使うデータはmemcacheに突っ込んどけば良いんじゃないかな?
    1. 更新頻度の問題がある。
      1. インスタンスが停止する場合。→所詮その程度なので安く収まるのでは?
      2. 結局は運用してみないとわからない。
  5. 課金設定にしたら、9ドル/月
    1. 最大9ドル/日と勘違いしていた。(1日9ドルだと月2万以上…)
    2. 無料枠を使い切る→9ドルの枠でがんばる→枠を越えたら…。
  6. Javaで作ったアプリケーションの最初の起動は遅い…
    1. Javaは初回のアクセス(インスタンスを起動する)時は一呼吸あるけど、Goだとそれはない。
最後にFileUploadの話題

  1. FrontControllerで、Content-Typeを確認して、"multipart/"で始まるかどうかをチェック。
    1. "multipart/"の場合、ファイルがアップロードされてきた判定。
      1. この場合、MultipartRequestHandlerのインスタンスが生成される。
    2. その他は、普通の(?)リクエスト
  2. MultipartRequestHandler#handleで、FileUploadクラスのインスタンスを生成する。
    1. FileUploadクラスで、ファイルのサイズの制限設定を読み込む。
      1. 現時点のblankプロジェクトの設定はコメントアウトされているので初期値が使われる。
      2. 初期値は-1なので、「制限なし」
    2. この中で、いろいろな処理をして、FileItemクラスのインスタンスを生成する。
      1. MultipartRequestHandlerはかなり頑張ってます。
      2. ちなみに、ファイルを読み込む時のバッファは8 * 1024(8192)バイトです。ファイルが大きいと実行されるステップ数も増えます。
    3. 最終的にControllerでFileItem item = requestScope("formFile")の1行でファイルが取得できる。
  3. Serviceでアップロードされたデータを格納する処理を実行すれば、Datastoreにファイルが格納される。
個人的に思うこと。

  1. BlobstoreAPIを利用したほうが良い。
  2. ただし、BlobstoreAPIはファイルの中身をチェックできない
    1. UploadはAPI用のリンクへのリクエスト
    2. downloadは直接HttpResponseに書きこまれる
  3. したがって、中身をチェックしたい場合は、Slim3のFileUploadを使うほうが良い。
  4. あと、課金状況で判断するのも必要かもしれない。
  5. 32Mバイトを超えるファイルを格納する可能性があれば、BlobstoreAPIは使えないのでSlim3のFileUpload一択。(※ただし、そんなに大きなファイルを格納しようと思うと、30秒の時間制限を考える必要がある)

次回の#7は2/7(火)19:00〜の予定です。
少人数で行っているので制限が厳しいですが、ご参加をお待ちしております。

2012年1月25日水曜日

Slim3 Source Code Reading #5

今日は、Slim3 Source Code Reading #5の日でした。

今回も@sinmetalさんと2人での開催となりました。
内容は、

  1. 私の仕事でGoogle App Engineを使ったアプリケーションを作ることになったのでその質問
  2. Memcache.javaを読み進める。
  3. UserServiceを使った認証についての雑談
  4. org.slim3.datastoreパッケージのCreationXX.javaについて

でした。

1.は単純に仕事で使う事になった(した)ので、アプリケーションを開発している話を少ししました。

2.が本題のMemcacheの機能です。
方向性として、Memcacheの存在確認、MemcacheへのGet、Putを中心に読みました。
(今、deleteも確認しましたが、deleteもラッパーでした)
読み進めた所、Memcacheの存在確認はcontains(key)、Memcacheへの登録はput()を
それぞれ呼び出しますが、中身は、MemcacheServiceのメソッドを呼び出している
だけでした。

しかし、Getだけはそのままではなく、MemcacheDelegate.javaの214行目から
始まる、getInternal()でいろいろ処理されています。
MemcacheServicePb.MemcacheGetRequestクラスのインスタンスと、
MemcacheServicePb.MemcacheResponseクラスのインスタンスを生成し、
233行目のmakeSyncCall()を呼び出します。

このmakeSyncCall()は中で、ApiProxy.makeSyncCall()を呼び出し、
(恐らくSDKよりも下の)APIを呼び出し、Memcacheに登録しています。
MemcacheはKey-Value形式で、しかも、Valueはbyteデータを格納する事に
なっていますので変換処理が必要ですが、オブジェクトをbyteデータに変換する処理も
このあたりで行われているようです。

なぜ、このようにGetだけ、ただのラッパーではなかったのかは、分かりませんでした。
(恐らく、AppEngineのAPIを実行すると処理が遅いとか、無駄な待ち時間がかかるなど
あるのだろうと判断して終わらせました)

その次は、Memcacheの次に読み進めるのをどれにするか選んでいた時に、
ユーザ認証の話になり、私が、UserServiceのすばらしさを一方的に話した感じでした。

利点として、

  1. Googleの認証機構に全てお任せできる。
  2. 認証済みであれば、ユーザ情報が取れるので、利用できる。(Mail APIによる通知や、ユーザの識別、データの管理、アクセス管理など)
  3. データストアに(多分)SessionIDなどを登録する必要がなくなる。


1つ目は自力で認証処理を作る手間が省ける。かつ、(多分)自分よりも質の良いセキュリティが確保できる。しかも、Gmailが見れる状態(すでにログイン済み)だと、
アプリケーションの認証の処理が不要な場合もでてくる(入力の手間が減る)。
2つ目は自力認証の必要が無いので、ユーザ情報の有無で表示の制御ができる。何らかの処理の完了通知などにも利用できる。限られたユーザのみ見せるという機能も簡単に作れる。しかも、認証情報をデータストアに残す必要もないので、データストアのオペレーションの削減に繋がる。
3つ目はSlim3だとSession情報をデータストアに格納している(?)ということなので、
cronなどで、データストアの不要な情報を削除する処理を実行しなければならないが
その手間が省ける。データストアのオペレーションにも影響する。

ということが考えられます。

最後に4つ目のCreationXX.javaについてですが、org.slim3.datastoreパッケージに
CreationUser.java
CreationDate.java
CreationEmail.java
ModificationUser.java
ModificationDate.java
ModificationEmail.java

というファイルが存在しています。
このファイルはAttributeListenerインターフェイスを実装しているのですが、
何のためにあるのかよくわからないファイルでした。

が、実は(ドキュメントにも書かれているのですが)、アノテーションを設定すると、
ModelMetaで、データストアにPutする前に、現在の日付や、ユーザ情報を自動的に
取ってきて、プロパティに設定してくれます。
(Serviceで設定する必要がありません)

    @Attribute(listener = CreationUser.class)
    private User user;

上記のようにすることで、勝手に登録されます。
CreationXXとModificationXXの違いは、
CreationXXはすでに設定済みかどうかを判定し、設定されていない場合のみ登録されます。
ModificationXXの場合は、内容にかかわらず、値を上書きします。

次回は1/31(火) 19:00〜で、util系を読み進める予定です。

2012年1月18日水曜日

Slim3 Source Code Reading #4

今日はSlim3 Source Code Reading #4を開催しました。
参加者が私と@sinmetalさんの2人だったので、当初想定していたメンバーになりました。

今回は、

  1. EntityGroupとQueryについての前提知識の共有(というよりは、前回間違った説明があったのでその辺りの共有)
  2. Transaction、GlobalTransactionのソースコードを読み進めました。
  3. EntityGroupに関する実装上の話題。構成とか。Ancestorクエリにするケースなど。
  4. sinmetalさんの「サービス単体でテストコードを動かすと、Cross Group Transactionが正常に動作するのに、プロジェクト全体のテストを行うとテストに失敗する件」

を行いました。

Transactionについては、Datastore.javaにあるDatastore#beginTransaction()を読み進めました。
ただ、この中身は、Google App EngineのTransactionAPIを利用しているだけで、Slim3で特別何かをしているということはありませんでした。
ただし、TransactionのOptionでSystem.getProperty("slim3.useXGTX")の結果がTrueである場合、Cross Group Transactionが有効になります。
System.getProperty("slim3.useXGTX")はappengine-web.xmlに記述してある値を取得します。※現時点ではデフォルトでは記述がないので、値はfalseです。

GlobalTransactionに関しては、かなり大変な事をしてあるので、概要だけ説明を書いておきます。
(sinmetalさんが検証コードを書いて試してくれるという事なので、その結果を待ちます)
実際、GlobalTransactionが必要になる事はこれからは少なくなってくると思いますので
(現時点で既にDeprecated)Cross Group Transactionを利用した方が良いと思います。

具体的にはどうしているかと言うと、

  1. トランザクションを開始する。Datastore.beginGlobalTransaction();
  2. エンティティをGet or Putする時、Lock用のキーMapに、現在のキーの祖先が存在しているかを確認する。存在していれば、例外をスローする。ただし、存在していても、キーのTimestamp+30秒を超えていれば無視する。
  3. GlobalTransaction#Get or Putを呼び出す。その時、対象のエンティティの祖先のキーを取得し、Lock用のMapに退避しておく。キーにはTimestampを保存しておく。また、Getの場合はKeyのみ、Putの場合はKeyとEntityのペアをJournal用のMapにPutする。
  4. トランザクションをCommit or Rollback(GlobalTransaction#commit、rollback)する。この時、GlobalTransactionの場合は、Journal用のMapからGet用のKeyを削除し、Put用のEntityをslim3.GlobalTransactionカインドにPutする。Get用のKeyにはEntityが登録されていないので、Entity==nullであれば、Get用のKeyと判定する。ただし、単一EntityGroupのみの場合(Lock用のMapが空)はLocalTransactionと判断しそのままCommitする。
  5. GlobalTransactionの場合、TaskQueueにキューを登録する。登録するキューはGlobalTransactionServletへのリクエストとして登録する。
  6. GlobalTransactionServletで、RollForwardトランザクションの場合、Commitされた場合は、GlobalTransaction用のカインドにある、KeyとEntityのペアを取り出し、本来のカインドのエンティティにPutする。Rollbackトランザクションの場合は、カインドから対象のエンティティを削除する。

大体の処理は上記のようになっています。
正確な処理については、大変ですが、ソースコードを読んでみてください。

最後にsinmetalさんから相談された、「単体テストでHRDのテストが動作しない件」ですが、見せていただいたテストコードには、setUp()でSystem.setProperty("slim3.useXGTX", "true");の
記述がありましたが、この設定を反映するAsyncDatastoreDelegateが何度もインスタンスが生成されるようであれば良いのですが、メンバとして保持している、
Datastoreクラスがstaticであるため、実行開始後、1回しか設定を読み込まないようになっているようです。したがって、System.setProperty()が呼び出される前にDatastoreクラスのメソッドが呼び出されてしまうとその時点でデフォルトのトランザクションオプションが反映されてしまって、System.setProperty()が意味をなさなくなってしまいます。

これについても検証コードの結果を待ちましょう。

ただ、この後にも話をしたのですが、開発環境HRDのオプションの遅延は、
かなり大きいので、Queryのテストをしたい時にまでHRDの遅延が発生すると
テストが通らない問題に直面してしまいます。
一つは、遅延確率を設定するオプションを0にして遅延しなくするという方法を
提案しましたが、テストクラスの本数が多くなってくると面倒という問題があるようです。
また、Thread.sleep(2000)とか入れてみるという提案もしたのですが、
これも同様に本数が増えてくると、テストの実行時間が長くなってしまうので
全体のテストを行う時に問題になる。ということでした。

今日は、時間も長めにやりましたが、内容も濃かったです。
疲れました。

2012年1月14日土曜日

Go言語プログラミング入門の正誤表を更新しました。

Go言語プログラミング入門正誤表に追記しました。
http://takashi-yokoyama.blogspot.com/2011/12/go.html

appengine/datastoreパッケージのPropertyLoadSaverについて、
誤記があったので、詳細に説明をしておきたいと思います。

PropertyLoadSaverインターフェイスのドキュメントです。
http://code.google.com/intl/ja/appengine/docs/go/datastore/reference.html

書籍では、「データを登録した状態でないとプログラムが応答を返さなくなる」という
事が書かれているのですが、これは、サンプルコードを作成している時の試行錯誤の
途中段階の誤認識をした状態で記述をしていました。

実は、PropertyLoadSaverインターフェイスのLoadメソッド、Saveメソッドの定義は
以下の通りです。(書籍のサンプルコードから抜粋したものです)
func (g *Guest) Load(c <-chan datastore.Property) os.Error {
func (g *Guest) Save(c chan<- datastore.Property) os.Error {
どちらもchannelが引数になっています。このchannelについての説明はP.63付近に
少し記述をしていますが、CriticalSectionと同じで、自分のスレッドをブロックします。
ブロックが解除されるのはchannelに値がセットされた時か、close(c)された時と
なります。LoadStruct()、SaveStruct()はメソッド内でchannelの処理を行っているため、呼び出せば構造体に値がセットされ、channelの処理も適切に行ってくれます。

書籍のプログラムだと、Saveメソッドの方では、何もしていないため、channelのブロックが解除されないことになり、現時点ではプログラムが応答を返さなくなってしまいます。

したがって、Load()の方でも、同様に「何もすることがない」からといって何もしないと
プログラムが応答を返さなくなってしまいます。

2012年1月12日木曜日

AppEngine Office Hourに参加した。

Japanese AppEngine Office Hourに参加したので、そこでの質問と回答をメモ
https://plus.google.com/u/0/110554344789668969711/posts/d3ykURrCnDt

Python2.5以下でないとssl警告が出る件
→Goのappcfg.pyはpython2.5が指定されているので、
$ python ../appcfg.py update app
など、python新しいバージョンのPythonを明示的に指定すると、警告はでない。
または、python2.5の方にsslモジュールを適用する。

ユーザ名を取得する場合。
初回ログイン時に表示名を入力するなどしてもらって、表示名を入手する。
ATNDとかどうだったかな…?
→多分、同じ動作をさせていると思われる。
→確認したらユーザ設定画面があって、そこでユーザ名を登録していた。

Emailの先頭部分(@以前)を表示
→だと、メールアドレスがバレてしまうので…。
上記の対応を行うしか無いだろう。

Java、PythonもNicknameを取得すると、メールアドレスの先頭部分が取れるかも。

Goを使うメリット
・ネイティブであるので、立ち上がりが一番速い。メモリの効率も良い。

XGTransactionの最大エンティティグループについて(仕様で最大5グループまで)
http://code.google.com/intl/en/appengine/docs/go/datastore/overview.html#Cross_Group_Transactions

開発環境でHRDのオプションを入れるコマンド
$ python ../dev_appserver.py --high_replication ./app


2012年1月11日水曜日

Slim3 Source Code Reading #3

Slim3ソースコード読書会(3)を開催しました。

今回は、前回のPutとかぶっていますが、以下のソースコードを読みました。
Datastore.java
DatastoreDelegate.java
AsyncDatastoreDelegate.java
ModelQuery.java
AbstractQuery.java
DatastoreUtil.java

そこで話題になったポイントをメモしておきます。

1点目。
AsyncDatastoreDelegate.javaの642行目で、

mm.validateKey(key);を呼び出し、KeyのKindとModelMetaで保持しているKindを
比較し、一致しなければIllegalArgumentExceptionをスローしている件。

これは、以下のように、取得しようとしているModelとKeyが一致しない時などに、
エラーにするための防波堤ではないか。という結論になりました。

例えば、以下のようにTweetクラスとTweet2クラスが存在したとして、
プロパティの構成が全く同じ場合で、データストアにKeyが存在している場合。

Tweet tweet = new Tweet();
Datastore.put(tweet);
Tweet2 tweet2 = Datastore.get(Tweet2.class, tweet.getKey());    // 指定したKeyとModelが不一致

データストアからのGetは成功しますから、構成が同じであれば、Tweet2クラスの
Modelとして値が取得されます。しかし、これは本来Tweetのデータであるため、
Modelが取れてはいけないのですが、データストアの仕組み上防ぐことができません。
したがって、ModelMetaで定義しているKindとKeyのKindを比較して一致しているか
どうかを確認していると思われます。

2点目。
ModelQuery.javaの241行目のasList()で最後にfilterInMemory()とsortInMemory()を
呼び出していることについて。

最初は、「DatastoreのQueryでsortやFilterを指定したらメモリ上でのソートやFilterは不要だろう」と
いう意見だったのですが、よく読んでみると、呼び出し元でfilterInMemory()を
呼び出していなければ、inMemoryCriteriaのSizeが0なので、実際には
Filter処理は実行されません。同様に、sortに関してもinMemorySortCriteriaの
Sizeが0であればSort処理も実行されません。

ただし、データストアでSortを実行したあと、sortInMemoryを呼び出していれば、
両方とも実行されることになります。

恐らく、inMemoryFilterについてはOR条件で抽出したい場合があれば、
それを実現するために利用するのではないかという結論になりました。
(AND条件での検索であれば、QueryにFilterを設定するだけで良いが、データストアでOR条件は指定できない。)

次回は1/17(火)19:00〜の予定です。