Google App Engine 実践リファレンスを読んでみた
slim3 を趣味でやっているものの、本業(プロジェクト管理&SAStrutsでの開発)が忙しく、なかなか GAE に時間を割けない昨今。
一念発起して、『Google App Engine 実践リファレンス』を購入して読んでみました。
- 作者: 清野克行
- 出版社/メーカー: 技術評論社
- 発売日: 2010/01/05
- メディア: 単行本(ソフトカバー)
- 購入: 2人 クリック: 111回
- この商品を含むブログ (11件) を見る
内容としては…
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つは、同じ意味になります。
- (String) request.getAttribute("abc")
- asString("abc")
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つめに、検証対象の入力値のキーを指定します。
- 引数の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の作成方法
slim3 の build.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 |
- JSP は、gen-controller タスクを実行する場合のみ生成されます。
- $root は、アプリケーションのルートパッケージを指します。
- war/index.jsp は、slim3 インストール時に既に作成済です。
簡単にまとめると、次のようなルールになります。
※URL のルールの詳細は、こちらをご覧下さい。
3.Indexとは
URL の最後に "/" を指定すると、IndexController というクラスが生成されます。
ところで、そもそも Index とは何でしょう?
これは、SAStruts でいうところの root への遷移を示すものです。
SAStruts の Action は、遷移先毎にメソッドを用意します。
そのうち、root(例えば OrderAction なら /order/)へ遷移するメソッドの名前は、index() とします。
slim3 では、遷移先毎にクラス(Controller)を用意します。
slim3 は SAStruts の影響を濃厚に受けているので、root へ遷移するクラス名を IndexController としている模様です。
root に遷移したい場合は、IndexController を呼び出せばOKです。
※他にも、slim3 にはところどころに
SAStruts の影響が見受けられます。
それを探してみるのも一興でしょう。
jsp-configは使えない?
slim3 で Service と Model ばかり作成していて、画面を作成していないことに気がついた今日この頃。
JSP や CSS を放置していたので整理を開始。(後日きちんとドキュメント化します。)
で、taglib などの JSP 共通定義をしようと、web.xml に jsp-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”というクラスを定義・使用します。
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 には大きく、
の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);
4.単体テスト
Service の単体テストは、org.slim3.tester.LocalServiceTestCase クラスを継承して作成します。
このクラスは、次の2つの機能を提供してくれます。
(1)Datastore/Key を使用できる
内部で Thread/ApiProxy を操作し、Datastore/Key を使用できる環境を提供してくれます。
※前回の記事を参照。
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 は、slim3 や BigTable で使用されることから、単なる JavaBeans には見られない以下のような特徴を持ちます。
※以下は、Model 生成時に自動的に用意されます。
(1)java.io.Serializable を implement している
あわせて、定数 serialVersionUID が定義されます。
(2)クラス定義に @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.データ項目の追加方法
永続化対象のデータ項目を追加する方法は、次の通りです。
ちなみに、使用できるデータ型は、こちらに定義されています。
なお、変数としては定義したいけれども永続化対象にはしたくない!という時には、変数定義に @Attribute(persistent=false) のアノテーションを定義すればOKです。
データ項目の追加例は、当記事の一番下にある定義例をご確認下さい。
5.Metaクラスの自動生成
Model クラスをコンパイルまたは実行すると、<プロジェクトルート>/.apt_generated/<アプリケーションのルートパッケージ>/meta 以下に、
ここに、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つのポイントがあります。
- TestCase を継承しない
- アノテーションを使用する
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 は複数指定可能
実行順序が問題。
後で調べます。