ログ日記

作業ログと日記とメモ

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で利用可

こんな感じ。

Slim3のアップグレードメモ

  1. 新しい slim3-xxx.jar を /war/WEB-INF/lib に置きます。
  2. 新しい slim3-xxx.jar を CLASSPATH に追加します。
  3. 古い slim3-xxx.jar を CLASSPATH から削除します。
  4. 古い slim3-xxx.jar を削除します。
  5. 新しい slim3-gen-xxx.jar を /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 + Slim3 で JUnitのログレベルってどうやって変えるんだろう?

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メソッドに書いておくことにした。

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にしようか。
そうすると並べ替え用のプロパティが必要か…。もう少し考える。

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を持っておくのがやっぱり安全でいいのかもしれない。

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 インスタンスの表現に変換します。

http://code.google.com/intl/ja/appengine/docs/java/javadoc/com/google/appengine/api/datastore/KeyFactory.html

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


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



http://code.google.com/intl/ja/appengine/docs/java/datastore/creatinggettinganddeletingdata.html
ここにはキーは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が欲しいところだ。カウンタ用モデルを作るのは避けたいが…いっそのことユーザー別に全エンティティに対応するカウンタを作ろうか。ページャのためのカウントにも使えて一石二鳥かもしれない。ユーザー別だから全部同じモデルに突っ込んでも更新が重なることはなさそうだし。

もうちょっと考える。

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がサポートされているっていうのはいいね。