The HIRO Says

If you smell what The HIRO is cooking!!!

Google App Engine 実践リファレンスを読んでみた

slim3 を趣味でやっているものの、本業(プロジェクト管理&SAStrutsでの開発)が忙しく、なかなか GAE に時間を割けない昨今。


一念発起して、『Google App Engine 実践リファレンス』を購入して読んでみました。

Google App Engine 実践リファレンス

Google App Engine 実践リファレンス


内容としては…
UI 系が全て JavaScript の非同期処理で記述されているのはどうかと思いましたが、
(1)JDO(JDOQL)での BigTable へのアクセス方法、(2)エンティティグループの意味(Transaction を使うのはどういう時か)、あと(3)memcache などのサービスについてざっと記載されているので、私的には非常に助かりました。
プログラムを書く時間が足りなくなったときに知識を補うには、非常によい書籍だと思いました。

【slim3】画面遷移のための”Controller”−(2)実装方法

前回slim3 の画面遷移制御用コンポーネントである
”Controller”の生成方法をまとめました。
今回は、この Controller の実装方法をざっと見てみます。


1.Controllerの基本的な構成

Controller の基本的な要件は、以下の3点です。
(1)org.slim3.controller.Controller を継承(extend)する。
(2)org.slim3.controller.Navigation を戻り値として返す
   run() メソッドをオーバーライドする。
(3)run() メソッド内で、遷移先のパスを返す。


ちなみにこれらは、Ant で自動的に生成されます。


2.パラメータの取得方法

画面などからの入力値を取得するには、
次の2つの方法があります。

(1)HttpServletRequest 等から取得する方法

通常の Servlet と同様、HttpServletRequest 等の getAttribute() メソッドなどを呼び出すことで、入力値を取得することができます。
HttpServletRequest 等へのアクセス方法は、
「3.HttpServletRequest等へのアクセス方法」をご参照下さい。

(2)Controller の専用メソッドを利用する方法

Controller クラスには、asString()asInteger() など、HttpServletRequest に格納された値を取得するためのメソッドが型毎に用意されています。
これらを呼び出すことで、入力値を取得することができます。


ちなみに次の2つは、同じ意味になります。

  1. (String) request.getAttribute("abc")
  2. asString("abc")
(3)【参考】ActionFormについて

slim3 には今のところ、Struts の ActionForm のような、画面入力値専用の JavaBeans クラスは用意されていません。
※そもそも作る予定があるのかは不明です…
 ひがさんのみぞ知る?それとも自力で作る?
 一応 Struts・S2・SAStruts のソースは知っているので、
 自力で作ろうと思えば作れると思いますが…
 今のところやる気出ず…(´・ω・)


3.HttpServletRequest等へのアクセス方法

HttpServletRequest・HttpServletResponse・ServletContext には、それぞれ専用のアクセス用クラスが slim3 で用意されています。

(1)HttpServletRequest

org.slim3.util.RequestLocator クラスの get() メソッドを呼び出します。

(2)HttpServletResponse

org.slim3.util.ResponseLocator クラスの get() メソッドを呼び出します。

(3)ServletContext

org.slim3.util.ServletContextLocator クラスの get() メソッドを呼び出します。

(4)【参考】HttpSession

HttpServletRequest の getSession() メソッドで取得します。


4.入力値の検証方法(validation)

入力値の検証は、org.slim3.controller.validator.Validators クラスで行います。
入力値の検証手順は、次の通りです。


(1)Validators のインスタンスを生成する。
引数には、HttpServletRequest、または検証対象が格納された Map を渡します。


(2)add() メソッドで、入力値とそれに対する検証方法を追加する。

  1. 引数の1つめに、検証対象の入力値のキーを指定します。
  2. 引数の2つめ以降に、検証方法を追加します。(可変長引数)


具体的には、以下の例をご確認下さい。

    Validators v = new Validators(RequestLocator.get());
    // 必須チェック・100 文字以内のチェックを実施
    v.add("name", v.required(), v.maxlength(100));

5.初期画面用のController

アプリケーションの初期画面表示を Controller で制御したい場合は、ルートパッケージ/controller ディレクトリに、IndexController クラスを用意します。


6.【参考】Controllerの実装例

package xxx.yyy.controller.order;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class OrderController extends Controller {

    @Override
    public Navigation run() {

        // 入力値検証(validation)
        Validators v =
                new Validators(RequestLocator.get());
        v.add("name", v.required(), v.maxlength(100));

        if (!v.validate()) {
            // 入力値検証失敗時
            // この呼び方だと、/war/order.jsp へ遷移する。
            return forward("order.jsp");
        }

        // 入力値検証成功時

        // 必要に応じて Service を呼び出し、
        // ビジネスロジックを実施する。
        new OrderService()
                .order(new Order(asString("name")));

        /*
         * この呼び方だと、
         * /war/WEB-INF/view/order/orderComplete.jsp
         * へ遷移する。
         */
        return forward(
                "/WEB-INF/view/order/orderComplete.jsp");
    }
}

次の予定

次は、Controller の UT について考えたいと思います。

【slim3】画面遷移のための”Controller”−(1)概要と生成方法

slim3 では、画面遷移の制御を、”Controller”というコンポーネントで行います。
Struts で言うところの Action に相当するもので、リクエストを受け付けて次の遷移先を返すという役割を持ちます。
ひとまず画面とその遷移を構築したい!という場合には、JSP と Controller を用意すればOKです。


1.Controllerの作成方法

slim3build.xml にある、以下のタスクを起動します。

タスク名 内容
gen-controller Controller・ControllerTest・JSP を生成します
gen-controller-without-view Controller・ControllerTest を生成します

こちらも参照。


gen-controller の場合、Controller とその遷移先の JSP を生成してくれます。
なので、基本的には gen-controller を使っていくことになります。


2.URL の指定方法

Controller 生成時、URL の入力を求められます。
この入力する URL により、Controller 及び JSP の名前と作成場所が以下のように決まります。

URL Controller
/ $root/controller/IndexController
/aaa $root/controller/AaaController
/aaa/ $root/controller/aaa/IndexController
/aaa/bbb $root/controller/aaa/BbbController
/aaa/bbb/ $root/controller/aaa/bbb/IndexController
/aaa/bbb/ccc $root/controller/aaa/bbb/CccController

※1行にまとまらなかったので分割しています。

URL JSP
/ war/index.jsp
/aaa war/aaa.jsp
/aaa/ war/aaa/index.jsp
/aaa/bbb war/aaa/bbb.jsp
/aaa/bbb/ war/aaa/bbb/index.jsp
/aaa/bbb/ccc war/aaa/bbb/ccc.jsp
  1. JSP は、gen-controller タスクを実行する場合のみ生成されます。
  2. $root は、アプリケーションのルートパッケージを指します。
  3. war/index.jsp は、slim3 インストール時に既に作成済です。


簡単にまとめると、次のようなルールになります。

  1. / で終わる場合、IndexController・index.jsp を生成します。
  2. / 以外で終わる場合、XxxController・Xxx.jsp を生成します。


※URL のルールの詳細は、こちらをご覧下さい。


3.Indexとは

URL の最後に "/" を指定すると、IndexController というクラスが生成されます。
ところで、そもそも Index とは何でしょう?
これは、SAStruts でいうところの root への遷移を示すものです。


SAStruts の Action は、遷移先毎にメソッドを用意します。
そのうち、root(例えば OrderAction なら /order/)へ遷移するメソッドの名前は、index() とします。


slim3 では、遷移先毎にクラス(Controller)を用意します。
slim3SAStruts の影響を濃厚に受けているので、root へ遷移するクラス名を IndexController としている模様です。


root に遷移したい場合は、IndexController を呼び出せばOKです。


※他にも、slim3 にはところどころに
 SAStruts の影響が見受けられます。
 それを探してみるのも一興でしょう。


4.hot deploy

GAE のローカルサーバ起動時、生成した Controller/JSP は、即座にアプリケーションに反映されます。
通常の JavaEE Web アプリケーションのように、いちいちサーバを再起動する必要はありません。
(この機能を、hot deploy と言います。)


今後の予定

今後は、以下の事項に触れる予定です。

  1. Controller の中身
  2. Controller の UT の実施方法
  3. JSP の作成方法・ルール

jsp-configは使えない?

slim3 で Service と Model ばかり作成していて、画面を作成していないことに気がついた今日この頃。
JSPCSS を放置していたので整理を開始。(後日きちんとドキュメント化します。)


で、taglib などの JSP 共通定義をしようと、web.xmljsp-config を定義したところ…
まったく反映されない(´・ω・)


↓どうも既知の問題として認識されているようです。
http://groups.google.co.jp/group/google-appengine-java/browse_thread/thread/2bfc201cd7ad8b48?pli=1

【slim3】ビジネスロジックとしての”Service”

前回紹介した Model は、あくまで JavaBeans であり、それ自体に永続化の機能はありません。
BigTable への Model の永続化、および BigTable からの Model の取得といった persistence に関する処理は、Model 以外の別のクラスで定義する必要があります。
また、FrontController(後述)は画面遷移を扱うクラスであり、Struts の Action クラス同様、ビジネスロジック及び persistence の処理を定義すべきではありません。
slim3 では、ビジネスロジック及び persistence の処理を行うクラスとして、”Service”というクラスを定義・使用します。


1.Serviceの作成方法

こちらの記事の、”gen-service”をご確認下さい。
※gen-gwt-service については、後日触れます。


2.Serviceの概要

Model とは異なり、Service にはアノテーションなどの slim3 特有の仕様・縛りはありません。
(後述しますが、UT には slim3 特有の仕様・縛りがあります。)
Service では、ビジネスロジック及び persistence の処理を、自由に記述することになります。


3.Datastoreによるpersistence

Model の persistence の処理は、org.slim3.datastore.Datastore クラスを利用して実装します。
Datastore クラスの API のうち、主だったものを以下に列挙します。

メソッド名 内容
get BigTable から Model を取得する(select)
put BigTable へ Model を永続化する(insert/update)
delete BigTable から Model を削除する(delete)
createKey Model の Key を生成する
allocateId Model の Key を生成する
beginTransaction トランザクションを開始する
commit トランザクションをコミットする
rollback トランザクションロールバックする


Datastore には大きく、

  1. Model の CRUD
  2. Model の Key の生成
  3. トランザクションの制御

の3つの機能があります。

例1.Modelの登録

Q&A を表す Model を、BigTable へ登録する例です。

    QAndA model = new QAndA();

    // キー生成
    model.setKey(Datastore.createKey(QAndA.class, 1L));
    model.setQuestion("GAE の意味は?");
    model.setAnswer("Google App Engine です。");

    // Model 登録
    Datastore.put(model);
例2.Modelの取得

Q&A を表す Model を、BigTable から取得する例です。

    // Key に該当する Model を取得
    Datastore.get(
            QAndA.class,
            Datastore.createKey(QAndA.class, id));

なお、データが見つからない場合は、org.slim3.datastore.EntityNotFoundRuntimeException がスローされます。

例3.トランザクションの制御
    Transaction tx = Datastore.beginTransaction();

    try {
        Datastore.put(model);
        Datastore.commit(tx);

    } catch (Throwable th) {

        Datastore.rollback(tx);
    }

ちなみに Transaction は、com.google.appengine.api.datastore.Transaction クラスを使用します。


4.単体テスト

Service の単体テストは、org.slim3.tester.LocalServiceTestCase クラスを継承して作成します。
このクラスは、次の2つの機能を提供してくれます。

(1)Datastore/Key を使用できる

内部で Thread/ApiProxy を操作し、Datastore/Key を使用できる環境を提供してくれます。
前回の記事を参照。

(2)テスト終了後、トランザクションロールバックしてくれる

S2Unit の xxxTx() と同様、テスト実行後にデータの登録・更新・削除をなかったことにしてくれます。
これにより、単体テストの連続実行が可能となります。


追記
ひがさんによると、最新版だと LocalServiceTestCase が AppEngineTestCase に名前が変わっているそうです。
(Production Server 上でもテストできるようにすることが目的だそうです。)


5.単体テストの実装例

以下のメソッドを持つ、QAndAService というクラスの単体テストを行うこととします。

メソッド名 処理内容
registerQAndA() Q&A の Model を、BigTable へ登録する
findQAndAById() Key に該当する Q&A の Model を、BigTable から取得する
package xxx.yyy.service.qanda;

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slim3.datastore.Datastore;
import org.slim3.datastore.EntityNotFoundRuntimeException;
import org.slim3.tester.LocalServiceTestCase;

import com.google.appengine.api.datastore.Key;
import xxx.yyy.model.qanda.QAndA;

public class QAndAServiceTest
        extends LocalServiceTestCase {

    // テスト対象クラスのインスタンス。
    private QAndAService target;

    @Before
    public void before() {
        this.target = new QAndAService();
    }

    @After
    public void after() {
        this.target = null;
    }


    // registerQAndA()+findQAndAById() の正常系
    @Test
    public void registerQAndA() {

        QAndA model = new QAndA();
        Key key = Datastore.createKey(QAndA.class, 1L);
        model.setKey(key);
        model.setQuestion("foo");
        model.setAnswer("bar");

        target.registerQAndA(model);

        // データが想定通りに登録できたことの確認
        QAndA result = target.findQAndAById(1L);
        assertEquals(
                key,
                result.getKey());
        assertEquals(
                "foo",
                result.getQuestion());
        assertEquals(
                "bar",
                result.getAnswer());
    }


    // findQAndAById() の異常系
    @Test(expected=EntityNotFoundRuntimeException.class)
    public void findQAndAById_DataNotFoundError() {
        target.findQAndAById(1L);
    }
}

【slim3】永続化対象としての”Model”

GAE ではデータを、BigTableという key-value ベースの DB(key-value store)で管理します。
この”BigTable”へ永続化するクラスとして、slim3 では”Model”というクラスを定義・使用します。


1.Modelの作成方法

前回の記事の、”gen-model”をご確認下さい。


2.Modelの概要

Model は、一言でいうと JavaBeans です。
基本的に、永続化対象のデータ項目をインスタンス変数として定義し、その getter/setter を定義すればOK、という作りになっています。


3.Modelの詳細

Model は、slim3BigTable で使用されることから、単なる JavaBeans には見られない以下のような特徴を持ちます。
※以下は、Model 生成時に自動的に用意されます。


(1)java.io.Serializable を implement している
あわせて、定数 serialVersionUID が定義されます。


(2)クラス定義に @Model アノテーションがつく

  • このアノテーションにより、Meta クラス(後述)が生成されます。
  • org.slim3.datastore.Model クラスを参照。


(3)主キーを持つ

  • com.google.appengine.api.datastore.Key 型の変数 key を持ちます。
    • Model クラスには、主キーが1つ必要。
  • 同変数には、@Attribute(primaryKey=true) アノテーションがつきます。
    • org.slim3.datastore.Attribute クラスを参照。


(4)楽観チェック用の version を持つ

  • Long 型の変数 version を持ちます。
  • 同変数には、@Attribute(version = true) アノテーションがつきます。
    • org.slim3.datastore.Attribute クラスを参照。


(5)schemaVersion を持つ。

  • Integer 型の変数 schemaVersion を持ちます。
  • ※意味がよく分からないです。。。
    • GAE バージョンアップに備え…という情報があるが、裏がまだとれていないです。

※2010/02/07(日)追記
「データ定義」のバージョンを保持する項目(SVNバージョン番号みたいなもの)。
例えばデータ項目を追加した場合に、古い schemaVersion のレコードに一律にデフォルト値をセットする、というような使い方ができる。
※bluerabbitさん、ご指摘ありがとうございました。


4.データ項目の追加方法

永続化対象のデータ項目を追加する方法は、次の通りです。

  1. private なインスタンス変数を定義します。
  2. 上記のインスタンス変数の getter/setter を定義します。


ちなみに、使用できるデータ型は、こちらに定義されています。


なお、変数としては定義したいけれども永続化対象にはしたくない!という時には、変数定義に @Attribute(persistent=false)アノテーションを定義すればOKです。


データ項目の追加例は、当記事の一番下にある定義例をご確認下さい。


5.Metaクラスの自動生成

Model クラスをコンパイルまたは実行すると、<プロジェクトルート>/.apt_generated/<アプリケーションのルートパッケージ>/meta 以下に、Meta.java というクラスが作成されます。
ここに、Model の詳細実装が入っています。


データ項目を追加・変更してコンパイルすると、この Meta クラスが自動的に修正されます。


6.単体テスト

IndexController や Service とは異なり、特別なクラスの継承は不要です。
テスト内容としては、普通の JavaBeans のテストで問題ありません。


ただ1点だけ、テスト用に Key を生成する方法がわかりません。
無理やり Key を生成しようとすると、以下のエラーが出てしまいます。

java.lang.NullPointerException:
No API environment is registered for this thread.

これについては、引き続き調査をしていきます。
※2010/02/07(日)追記
テスト実施用に環境設定を行う必要がある。
暫定的な対応としては、org.slim3.tester.LocalServiceTestCase を extend すると回避可能。
(後日、正式な調査・報告をします。)
※bluerabbitさん、ご指摘ありがとうございました。


7.【参考】Model の定義例

「質問」と「回答」の2つのフィールドを持つ、Q&A の Model を作成する例を以下にあげます。

package xxx.yyy.model.qanda;

import java.io.Serializable;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Model;

import com.google.appengine.api.datastore.Key;

@Model
public class QAndA implements Serializable {

// fields
    private static final long serialVersionUID = 1L;

    @Attribute(primaryKey = true)
    private Key key;

    @Attribute(version = true)
    private Long version;

    private Integer schemaVersion = 1;

    // 追加したフィールド
    private String question;
    private String answer;



// getter
    public Key getKey() {
        return this.key;
    }

    public Long getVersion() {
        return this.version;
    }

    public Integer getSchemaVersion() {
        return this.schemaVersion;
    }

    // 追加した getter
    public String getQuestion() {
        return this.question;
    }

    // 追加した getter
    public String getAnswer() {
        return this.answer;
    }


// setter
    public void setKey(Key key) {
        this.key = key;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

    public void setSchemaVersion(Integer schemaVersion) {
        this.schemaVersion = schemaVersion;
    }

    // 追加した setter
    public void setQuestion(String question) {
        this.question = question;
    }

    // 追加した setter
    public void setAnswer(String answer) {
        this.answer = answer;
    }
}

【slim3】jUnit4.x系の使い方

前回も触れましたが、slim3 のテストでは jUnit 4.7 を使用しています。


jUnit は、4.x 系になってから、仕様がアノテーションベースに変更になっています。
前回はうまくまとめられなかったので、今回は jUnit 4.x 系の使い方を簡単にまとめてみようと思います。


ポイント

4.x 系の場合、大きく2つのポイントがあります。

  1. TestCase を継承しない
  2. アノテーションを使用する
TestCaseを継承しない

4.x 系では、@Test アノテーション(後述)をテストメソッドにつければテスト対象として認識されます。
そのため、3.x 系までのように org.junit.TestCase クラスを継承する必要はありません。
※特別にクラスを継承することは可能。Service のテストで触れる予定。


但し、TestCase クラスを継承しなくなると、assertXxx() などのメソッドを修飾子なしで呼び出すことができなくなり面倒です。
(例えば、assertXxx() は本来 org.junit.Assert クラスのメソッドで、TestCase がラップしているため、修飾子なしで呼び出すことができます。)
そのため 4.x 系では、以下のように、該当の static インポートを追加することになります。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;


slim3 の環境構築手順で、org.hamcrest.CoreMatchers などをコンテンツアシストに追加したのは、
上記に円滑に対応するためです。(たぶん)


アノテーションを使用する

4.x 系では、以下のアノテーションをメソッドに付記して、テストコードを作成していきます。

アノテーション 意味 FQN
@Test テスト対象メソッド org.junit.Test
@Test(expected=例外クラス.class) 指定した例外が発生したらOK org.junit.Test
@Test(timeout=ミリ秒) 指定時間をオーバーしたらNG org.junit.Test
@Ignore("comment") 一時的にテスト対象外とする(@Testにつける) org.junit.Ignore
@Before これまでの setUp() に相当するもの org.junit.Before
@After これまでの tearDown() に相当するもの org.junit.After
@BeforeClass 全テスト実行前に1回だけ実施される前処理 org.junit.BeforeClass
@AfterClass 全テスト実行後に1回だけ実施される後処理 org.junit.AfterClass

プログラム例と実行結果例

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

// モデル Order のテストという前提
public class OrderTest {

    // テスト対象
    private Order target = null;

    @Before
    public void before() {
        System.out.println("setUp");
        this.target = new Order();
    }

    @After
    public void after() {
        System.out.println("tearDown");
        this.target = null;
    }

    // 全テストで1回だけ実施
    @BeforeClass
    public static void beforeAll() {
        System.out.println("beforeAll");
    }

    // 全テストで1回だけ実施
    @AfterClass
    public static void afterAll() {
        System.out.println("afterAll");
    }


// ↓テストケース
    // 通常のケース
    @Test
    public void test() throws Exception {
        assertThat(this.target, is(notNullValue()));
    }


    // 例外発生の検証
    @Test(expected=NullPointerException.class)
    public void expectError() {
        this.target = null;
        this.target.toString();
    }


    // 無視の検証
    @Test(expected=IllegalArgumentException.class)
    @Ignore("test")
    public void unexpectError() {
        this.target = null;
        this.target.toString();
        System.out.println("This case will be ignored");
    }


    // 想定時間内に処理が終了することの検証
    @Test(timeout=1000)
    public void testTimeout() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("sleeping...");
        }
    }
}
実行結果
beforeAll
setUp
tearDown
setUp
tearDown
setUp
sleeping...
 〜(略)
tearDown
afterAll

気がついた点

(1)Before/After/BeforeClass/AfterClass は public
public にしないと、以下のエラーが発生します。
java.lang.Exception: Method xxx() should be public


(2)Before/After/BeforeClass/AfterClass は void
void にしないと、以下のエラーが発生します。
java.lang.Exception: Method xxx() should be void


(3)BeforeClass/AfterClass は static
static にしないと、以下のエラーが発生します。
java.lang.Exception: Method xxx() should be static


(4)Before/After/BeforeClass/AfterClass のメソッド名は自由
@Before で after() とか命名できますが…混乱するので避けましょうw


(5)Ignore は Before/After/BeforeClass/AfterClass には効かない
@Ignore は、テストメソッドにのみ効果があるようです。


(6)Before/After/BeforeClass/AfterClass は複数指定可能
実行順序が問題。
後で調べます。