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
ご興味がありましたらどうぞ。