より良い環境を求めて このページをアンテナに追加 RSSフィード

2011-07-26

[][][] Slim3のModelRefをGWTで使えるかどうかのユニットテスト

GWT上でModelRefが利用出来ることが分かったが、サーバー側で一度もgetModel()を実行せずにGWTで初めてgetModel()するともnullが返る。

気を付けていても忘れることがあって思わぬところでエラーが出る。

そんなときはテスト。


ModelRefは内部にModelインスタンスを保持していなければquery()を実行してデータストアから取得する。

そのquery()をmockitoのspyでチェックすれば良い。

class NodeServiceImplTest extends ServletTestCase {
    private NodeServiceImpl service = new NodeServiceImpl();

    @Test
    public void getForGWT() throws Exception{
        Node root = service.getTree("rootKey");
        InverseModelListRef<Node, Node> ref = spy(root.getChildrenRef());

        assertThat(ref.getModelList().size(), is(5));
        // getModelListはservice側で一度も呼ばれていなければquery()を呼び出す

        verify(ref, never()).query();
        // query()が呼ばれていなければGWTで利用可

こんな感じ。

2011-07-24

[] Slim3アップグレードメモ

  1. 新しい slim3-xxx.jar を <Project>/war/WEB-INF/lib に置きます。
  2. 新しい slim3-xxx.jar を CLASSPATH に追加します。
  3. 古い slim3-xxx.jar を CLASSPATH から削除します。
  4. 古い slim3-xxx.jar を削除します。
  5. 新しい slim3-gen-xxx.jar を <Project>/lib に置きます。
  6. 新しい slim3-gen-xxx.jar を注釈処理のファクトリパスに追加します。
  7. 古い slim3-gen-xxx.jar を注釈処理のファクトリパスから削除します。
  8. 古い slim3-gen-xxx.jar を削除します。
https://sites.google.com/site/slim3documentja/documents/how-to-upgrade

ここに書いてあることをやって、更にEclipse

プロパティー」「Java コンパイラー」「注釈処理」「ファクトリー・パス」のjarも更新する。

あとlibsrcのソースjarも更新してビルドパスの設定でソースjarを指定。

[] GAE/J + Slim3JUnitのログレベルってどうやって変えるんだろう?

war/WEB-INF/classes/logging.properties はテストのときは読んでくれない模様。

コンソールに

2011/07/24 14:24:13 com.google.appengine.api.datastore.dev.LocalDatastoreService load
情報: Time to load datastore: 25 ms

とか出るのが気になる。


java.util.logging.config.fileを設定してもダメで、AppEngineのEnvironmentのプロパティも使われてないような。

どこも見に行ってないんだろうか…?


結局分からず仕舞いで、仕方がないので

        Logger.getAnonymousLogger().getParent().setLevel(Level.WARNING);

とBeforeメソッドに書いておくことにした。

2011-07-22

[][] GAE/J で 一対多の get、query の速度

直感的にはModelにKeyのリストを保持しておいて、Datastore.get(Child.class, keys)をやった方が速いと思っていたがInverseModelListRefを使ったqueryの方が速かった。


と言っても大した量でテストしてないんだけど、一応メモを残しておく。

ツリー構造のディレクトリのようなエンティティを200ほど登録していて、そのうち5件だけ持ってくる。

親に子のModelのキーのリストを保持しておいてDatastore.getで取得する場合と、親のInverseModelListRefを使った場合。

        String ret = "";
        long start = System.currentTimeMillis();
        Node parent = Datastore.get(Node.class, rootNode.getKey());
        List<Node> list = Datastore.get(Node.class, parent.getChildKeys());
        long end = System.currentTimeMillis();
        ret = "Datastore.get " + (end - start) + "ms";

        parent = Datastore.get(Node.class, rootNode.getKey());
        start = System.currentTimeMillis();
        list = parent.getChildrenRef().getModelList();
        end = System.currentTimeMillis();
        ret += "\n InverseModelListRef  " + (end - start) + "ms";

appengineにデプロイ直後は

Datastore.get 17ms
InverseModelListRef 118ms

だけど、二回目からは

Datastore.get 15ms
InverseModelListRef 8ms

となった。


念のため逆順でやってみると

InverseModelListRef 74ms
Datastore.get 17ms

InverseModelListRef 9ms
Datastore.get 16ms

となった。


なんだかよく分からない。KeyのリストでDatastore.getする場合は大体一定時間だ。InverseModelListRefは、デプロイ直後だけ遅くて、二回目からはこっちの方が速い。

エンティティを全部消してから実行するとInverseModelListRefが遅くなったりならなかったり。ムラが激しい。


Bigtable内でデータの分散配置待ちなのか?と思ってデプロイ後にしばらく待ってから実行したら速くなった。つまりエンティティを一気に更新した直後はDatastore.get(keys)の速度は一定だけどInverseModelListRef.getModelList()は遅くなる、大量に更新した直後でなければInverseModelListRef.getModelList()の方が速い、ということでいいんだろうか。




テスト用のコードを書いたわけでもなく今書いてるアプリにちょっとしたコードを追加して測っただけなので…というか上限があるから大量のデータを突っ込んで真面目にテストするには気が引ける。


暫定の結論としては、InverseModelListRefよりList<Key>の方が速い気がしたが気のせいだった。ということで。

Keyのリストを持つのはやめて全部InverseModelListRefにしようか。

そうすると並べ替え用のプロパティが必要か…。もう少し考える。

2011-07-21

[][] Slim3 + GWT の ModelRef など

昨日書いた *1 ときにはModelRefはGWTで使えないと思っていたんだけど、modelRef.getKey().getName() は実行出来る。

ModelRefはタイプセーフを実現するためのクラスで、実際にはKeyを持っている。


そして普通にGWT側で modelRef.getModel() を実行してももちろんnullが返る。

しかし、server側でmodelRef.getModel() を実行してからクライアントに返却すると、なんとGWTでgetModel()が実行可能になる。

InverseModelListRefでも同様で、サーバー側でgetModelList()を実行していればGWT側で利用出来るようになる。

これはModelRefやInverseModelListRefが一度取得したModelを内部のフィールドに保持しているからで、GWTクライアントにもそのまま渡されるようだ。


というわけで、キーに文字列を使った場合に一意性が確保できないという問題は残っているが、ModelRefがGWTクライアント側で使えないというのは勘違いだったようだ。


関連エンティティのキーを文字列で保持しておくとデータストアに問い合わせなくても関連先の情報が意味のある形で表示できるから便利だという気がしたんだけど、id自動生成+ModelRefを持っておくのがやっぱり安全でいいのかもしれない。

2011-07-20

[][] GAE/J の キーについて

ちょっと混乱したのでメモ。昨日書いた *1 ことは若干勘違いが入っていた。

keyToString、stringToKey と 文字列のキーかどうかは関係ないんだね。

文字列のキーをkeyToStringしたところで、Keyインスタンス自体のエンコード済み文字列(?)が返ってくる。これはURLのGETパラメーターに渡して文字列からKeyを復元するときに利用出来る。

今見たらjavadocにも書いてあった。

static java.lang.String keyToString(Key key)

Key を Web セーフな文字列に変換します。

static Key stringToKey(java.lang.String encoded)

String で表現されている Key を Key インスタンスの表現に変換します。

Error 404 (Not Found)!!1

keyToStringから返ってくる文字列にはインスタンス全ての情報が詰まっているようだ。


それに対して文字列のKeyは、KeyFactory.createKey(kind, name) で生成するが、kindやもしあれば親キーの情報を自分で設定する必要がある。

設定したnameを取り出したいときはkey.getName()でOK。



Error 404 (Not Found)!!1

ここにはキーは4種類あるけど、これはJDOを使った場合のことで低レベルAPI的には全てKeyクラスになる?


GWTと同様にマニュアルAPI DOC

日本語のjavadoc英語のjavadoc は結構異なっている。





GWTでKey.getName()が使えることが分かったし、今作ってるアプリの構造を全体的に考え直さないといけないかもしれない。

keyに文字列を設定して、アプリ側でそれを表示するようにした場合、モデル同士の関連はModelRefではなくKeyを直接持っていた方が便利そうだ。GWTでModelRefは使いづらい。

一対多関連も、モデルAにモデルBのキーのリストを持つようにするとモデルAを取得しただけでモデルBを識別する文字列のリストが分かる。


そうすると、例えばユーザーごとに写真をアップロードするアプリを考えた場合に、写真に付ける名前をそのままキーにすると複数ユーザーで同時に使えない。写真をフォルダに入れる場合も、同じフォルダ名が使えない。

エンティティグループを使って ユーザー > フォルダ1 > フォルダ2 > 写真 のようにしたい。が、このときトランザクションは全く不要だ。

独自のルールで文字列を階層化するのがいいのかな。


例えば

ユーザー1:仮ID 1001
フォルダ1:仮ID 1001
フォルダ2:仮ID 1002
写真1

 ↓

1001:1001:1002:写真1

のようにするとか。写真1のキーからはフォルダ1などの文字列を取得できる必要はなく(というかツリービューのようなもので開いていくなら既にどこかにデータがあるはず)ただ一意なキーを保てれば良い。

IDと文字列の両方を指定できるKeyが欲しいところだ。カウンタ用モデルを作るのは避けたいが…いっそのことユーザー別に全エンティティに対応するカウンタを作ろうか。ページャのためのカウントにも使えて一石二鳥かもしれない。ユーザー別だから全部同じモデルに突っ込んでも更新が重なることはなさそうだし。

もうちょっと考える。

2011-07-19

[][] GWTで使えるクラスのメモ

twitterにも書いたんだけど、どうしてGWTでKeyクラスが使えるんだろうと思っていた。

それから、Datastore.keyToString(model.getKey())をGWTクライアント側でやりたいよねと。


でも試しにmodel.getKey().getName()を実行してみたらちゃんと取得できた。

GWTクライアント側でもKeyクラスのメソッドは使えるようだ。

ということは、KeyがStringで生成されていることが確定しているならgetNameを呼び出せば元の文字列が取得できるわけだ。

わざわざサーバーに問い合せてサービスの方でkeyToStringを呼び出すか、Keyの文字列をコピーして別のフィールドに持たせようかと思っていたところだった。


何故だろうとソース探索。

GWTにはGAE/Jのコードは入ってなさそうだ。

それからslim3のコードを見ていくと、org.slim3.gwt.emul.S3Emulation.gwt.xmlのファイルがある。

そこには前に書いた super-source *1 があり、ルートが指定されている。

改めてよく見るとSlim3に com.google.appengine.datastore.Key_CustomFieldSerializer などのクラスがある。

KeyなどのappengineのクラスはソースファイルがないのでGWT用のJavaScriptは生成できないが、ここでシリアライザを使ってGWTでも使えるようにしているようだ。

公式にGWTがサポートされているっていうのはいいね。

2011-07-18

[][][] Slim3の本の途中まで読んだ感想と代わりのチュートリアル

一昨日書いたこの本。

1/3までじっくり読んであとは流しでしか読んでないけど、これは要らなかった。

読んでいて全然頭に入ってこない。

というか写経用なので、ただ読んでるだけっていう使い方は間違っているんだろう。

サンプルを元に読みながら作っていかないと意味ないっぽい。


だだ…変数名とかエンティティ名が気になる。

変数を大文字で始めていたりアンダースコアが入っていたり。

GWTの説明も特にない。コードは書いてあるのだが、何故そうなっているかの説明が全然ない。

それからUiBinderを使うならGWT Designerを使えばいいのに何故か自作ツールを勧めている。

そして、何故かマルチモジュール構成で作っている。一つの画面にhtmlが一つ。GWTではこういう作り方は普通やらないんじゃないかな。普通はHistoryHandlerを使うと思うんだけど。

switch文やif文の羅列も良くない。ボタンのクリック処理も何故UiHandlerを一カ所にまとめてClickEvent#getSourceで分岐するんだろう。GWT Designerを使っていればそんなことにはならず、ちゃんとメソッドが分かれてくれる。


この本は写経用に書かれているのに、GWT+Slim3のための写経には全く適していないのだ。

Slim3の部分に関してはそんなに変なことは書いていないと思うのだが、全体的にGWTが絡んでくるサンプルになっているのでSlim3の部分だけ読むのも難しい。どこからどこまでがGWTSlim3かを意識させない作りになっていて、分からないままやっつけるのにはいいが理解の助けにするには向いていない。






これで終わるのも気が引けるので、自分的GWT+Slim3の始め方を書いておこう。


まず http://code.google.com/webtoolkit/doc/latest/DevGuideMvpActivitiesAndPlaces.html この辺のPart I から読む。

本はダメだと思う。どうしてもGWTの更新速度に追いつけない。本に載っているものがどんどん古い書き方になっていく。なので基本的に英語版Webのものを見る。

冗長でめんどくさいが、とりあえずMVPで作る。新しいGWTプラグインならMVPの新規作成に対応していて一気に複数のファイルを作ってくれるのでだいぶ楽になった。

このとき、UIGWT Designerを使う。あとRootPanelとRootLayoutPanelの違いを別途調べる。後でハマると面倒なことになる。


一通り理解したつもりになったら、Slim3へ。


Slim3https://sites.google.com/site/slim3documentja/getting-started-with-gwt ここを写経。ただしGWTのコードはさっきの書き方に従う。

例えば

    @UiHandler({"tweet", "refresh"})
    void handleClick(ClickEvent e) {
        if (e.getSource() == tweet) {
            tweet();
        } else if (e.getSource() == refresh) {
            getTweetList();
        }
    }
    
    void tweet() {
        service.tweet(content.getText(), new AsyncCallback<Void>() {
            
            public void onSuccess(Void result) {
                content.setText(null);
                getTweetList();
            }
            
            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }
        });
    }
    
    void getTweetList() {
        service.getTweetList(new AsyncCallback<List<Tweet>>() {
            
            public void onSuccess(List<Tweet> result) {
                table.clear();
                for (int i = 0; i < result.size(); i++) {
                    table.setHTML(i, 0, result.get(i).getContent());
                }
            }
            
            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }
        });
    }
https://sites.google.com/site/slim3documentja/getting-started-with-gwt/listing-tweets

ここのコードは

    @UiHandler("tweet")
    void onTweetClick(ClickEvent e){
        presenter.tweet(content.getText());
    }
    @UiHandler("refresh")
    void onTweetList(ClickEvent e){
        presenter.getTweetList();
    }

    public void showError(String err){
        Window.alert(err);
    }
    public void cleanContent(){
        content.setText(null);
    }
    public void setTweetList(List<Tweet> result) {
        table.clear();
        for (int i = 0; i < result.size(); i++) {
            table.setHTML(i, 0, result.get(i).getContent());
        }
    }

こんな感じになる。

presenterのgetTweetListが非同期呼び出しを実行し、viewのsetTweetListを呼び出す。

ViewにServiceを書かないようにするとだいぶすっきりする。


Activityはこんな感じ。

    private abstract class MyCallback<T> implements AsyncCallback<T> {
        @Override
        public void onFailure(Throwable caught){
            view.showError(caught.getMessage());
        }
    }
    public void tweet(String text) {
        service.tweet(text, new MyCallback<Void>() {
            @Override
            public void onSuccess(Void result) {
                view.cleanContent();
                view.getTweetList();
            }
        });
    }

MyCallbackはユーティリティ的なクラス。毎回エラー用処理を書くのが大変なので、復帰用の特別なコードがないなら共通化する。

これで一通り終了。


このあとはWindow.alertをGwtEventとEventHandlerに置き換えたり新しいPlaceを作って動きを確かめたり、Slim3トランザクションを試したり、など。GWTSlim3マニュアルから適当なところをかいつまんで書けば大体の動きが分かると思う。



ここまで勢いで書いたが、まだGWTSlim3アプリを作ったことはない。むしろ教えて欲しい。(そのために本を買ったのだった)