【入門編】Slim3で始める!GAE/JでWebアプリケーション開発 (第1回)

Serviceの作成 (その1)

次に、Modelをデータストアへ読み書きするためのServiceクラスを作成します。

作成するServiceの処理

アプリ要件から、Serviceクラスに以下の機能を実装します。

処理 利用する箇所
記事の全件取得 <トップ>の記事一覧の表示用
記事の新規登録 <記事作成>の登録用
記事の取得(主キー検索) <記事詳細>や<記事編集>の一件の記事表示用
記事の上書き更新 <記事編集>の更新用
記事の削除 <記事編集>の削除用
記事へのコメント登録 <記事詳細>のコメント登録用

作成手順

Serviceクラスはビジネスロジックを含むため、JUnitテストケースも併せて作成します。
Slim3を使うとテストも簡単です。 gen-xxx
のAntタスクでクラスを生成すると、自動的に対応するTestCaseクラスを作成してくれます。また、Slim3ではTDD(Test Driven
Development)という開発手法を推しているので、今回はそれに則った方法で開発していきます。

Service作成の流れは以下の通りです。

  1. Antタスク gen-service にてService、TestCaseの雛形を作成
  2. テストケースの作成
  3. 必要な処理の実装
  4. テストケースの実行

では、実際の手順を説明します。

  • Antタスク gen-service にてService、TestCaseの雛形を作成
    gen-service を実行し、Serviceクラスを作成します。Service名の入力プロンプトイメージすると、”simplebbs.bbs.BbsService”と”simplebbs.bbs.BbsServiceTest”の2クラスが作成されます。Serviceの雛形が作成されたイメージ
「記事の全件取得」の実装

まず最初に「記事の全件取得」処理から実装します。

  • テストケースの作成TDDの開発手法では先に必要な機能のテストケースを書きます。
    BbsServiceTestクラスのtest()メソッドに、以下のテストシナリオを追記しました。

    // 記事一覧の取得
    List<Head> headList = service.getAll();
    // ========== assertion start ========== //
    // 初回は0件であること
    assertNotNull(headList);
    assertTrue(headList.isEmpty());
    // ========== assertion end ========== //


    テストシナリオの記述イメージ

    BbsService#getAll()メソッドで記事を全件取得し、その結果が空のList(0件)であることをテストしています。また、このテストコードを書いた時点では、BbsService#getAll()メソッドは存在しないため未定義のエラーが出ていますが、次に実装します。

  • 必要な処理の実装「記事の全件取得」処理 BbsService#getAll()
    メソッドを実装します。Slim3でデータストアにアクセスするためには Datastore クラスを使います。そして、記事(Head)を全件取得するには、クエリ検索 Datastore.query() を使います。

    public List<Head> getAll() {
        HeadMeta m = HeadMeta.get();
        return Datastore.query(m).sort(m.postDate.desc).asList();
    }

    HeadMetaはHeadモデルのメタクラスです。
    @Modelアノテーションの付くModelクラスを作成すると、Slim3のアノテーションプロセッサによって自動的に「Model名+Meta」という名前で生成されるクラスです。Slim3の Datastore を使ってデータストアを操作する際、このメタクラスを使ってKIND(いわゆるテーブル)の指定や、絞込み条件(いわゆるWhere句)の指定などを行います。

    今回は「記事の全件取得」なので絞込条件は使いませんが、投稿日時降順で取得したいためソート順を指定しています。

    Datastore.query(m).sort(m.postDate.desc).asList();

    ここまで書いたらコードを保存しましょう。すると、BbsServiceTestクラスのエラーも解消されるはずです。

    ◆クエリ検索とは◆

    データストアに対し、主キー(Key)以外で検索を行う場合はクエリ検索を使います。Slim3のクエリは”流れるようなインタフェース”な構文で記述するため、非常に可読性が高く、理解し易いです。
    少し例を見てみましょう。

    ・記事全件をリストで取得

    HeadMeta m = HeadMeta.get();
    List<Head> resultList = Datastore.query(m).asList();

    ・投稿者”Hoge”の記事を、投稿日降順でソートされたリストで取得

    HeadMeta m = HeadMeta.get();
    List<Head> resultList = Datastore.query(m)
            .filter(m.username.equal("Hoge"))
            .sort(m.postDate.desc)
            .asList();

    1つ目の全件取得のクエリと、2つ目の絞込み&ソート指定されたクエリを見比べて下さい。 .filter() による絞り込み条件、
    .sort() によるソート条件が追加されているだけと、非常にシンプルにクエリが構成されています。

    また、クエリ内で指定するプロパティ名(username、postDateなど)は、メタクラスを使って指定するのでタイプセーフです。

  • テストの実施早速、テストを実行してみましょう。BbsServiceTestのエディタ上で 右クリック > Run As >
    JUnit Test をクリックします。
    JUnitテストの実行イメージ
    次のようにJUnitテスト結果がグリーンになれば成功です。
    JUnitテストの完了イメージ
    このようにTDDでは テストを作成 > 処理を実装 > テストを実施 の短い工程を繰り返しながら開発を進めていきます。ただ、このテストでは記事が一件もない状態でのテストしか行えていません。記事が一件以上ある状態でのテストもするべきでしょう。次に記事を登録するための処理を実装します。
「記事の新規登録」の実装
  • テストケースの作成処理の規模が小さいので先のシナリオに追加で作成しました。
    青字が手を加えた箇所になります。

    @Test
    public void test() throws Exception {
        assertThat(service, is(notNullValue()));
    
        // 記事一覧の取得
        List<Head> headList = service.getAll();
        // ========== assertion start ========== //
        // 初回は0件であること
        assertNotNull(headList);
        assertTrue(headList.isEmpty());
        // ========== assertion end ========== //
    
        // 記事の作成
        Head head = new Head();
        head.setSubject("初めての記事");
        head.setUsername("ユーザ1");
        head.setPostDate(new Date());
        // 本文の作成
        Body body = new Body();
        body.setText("初めての本文です。");
        // データストアへ更新
        service.insert(head, body);
    
        // 更新後の記事一覧の取得
        headList = service.getAll();
        // ========== assertion start ========== //
        // 投稿後の記事一覧は1件であること
        assertNotNull(headList);
        assertTrue(headList.size() == 1);
        // 以降、1件の中身のチェック
        Head storedHead = headList.get(0);
        assertNotNull(storedHead);
        // 記事タイトル、ユーザ名、投稿日時が正しく保存されているか
        assertEquals(head.getSubject(), storedHead.getSubject());
        assertEquals(head.getUsername(), storedHead.getUsername());
        assertEquals(head.getPostDate(), storedHead.getPostDate());
        // HeadからリレーションシップでBodyを取得できるか
        Body storedBody = storedHead.getBodyRef().getModel();
        assertNotNull(storedBody);
        // 本文が正しく保存されているか
        assertEquals(body.getText(), storedBody.getText());
        // ========== assertion end ========== //
    
    }

    テストの要約

    1. 記事一覧の取得(初回)
    2. 【検査】記事一覧は0件であること
    3. 記事の新規登録
    4. 記事一覧の再取得(登録後)
    5. 【検査】記事一覧は1件であること
    6. 【検査】取得した1件の記事が、先に登録した記事の内容と完全に一致していること

    ここで未定義となっている BbsService#insert(Head, Body) を、先と同じ手順で実装していきます。

  • 必要な処理の実装コードは以下の通りです。
    public void insert(Head head, Body body) throws Exception {
    
        head.setKey(Datastore.allocateId(HeadMeta.get()));
        body.setKey(Datastore.allocateId(head.getKey(), BodyMeta.get()));
        head.getBodyRef().setModel(body);
    
        Transaction tx = Datastore.beginTransaction();
        try {
            Datastore.put(tx, head, body);
            Datastore.commit(tx);
        }
        catch (Exception e) {
            if (tx.isActive()) {
                Datastore.rollback(tx);
            }
            throw e;
        }
    }

    ロジックについて説明します。

    Head、Bodyの業務要件的なプロパティの値(見出し、投稿者名、本文など)は予め呼び出し元でセットされるので、
    insert() メソッドの処理では

    1)データストア保存の前準備
    2)登録処理

    を行います。

    1)データストア保存の前準備

    1-1)Keyの設定

    データストアに保存するエンティティには主キーとなるKeyが必要です。Keyは整数値の id 、または文字列の
    name から生成します。 id を使う場合は自動採番することもできます。

    Headの主キーはアプリ要件的な意味は特にないので自動採番します。

    head.setKey(Datastore.allocateId(HeadMeta.get()));

    Datastore.allocateId() が id を採番してKeyを生成するメソッドです。採番される値は1から始まる連番とは限りません。

    次に、Bodyの主キーも採番しますが、 <span
    class=”important”>HeadのKeyを親キーに指定してKeyを生成 します。

    body.setKey(Datastore.allocateId(head.getKey(), BodyMeta.get()));

    これは、HeadとBodyを一つの更新単位でまとめて更新するために、エンティティグループ(以降、EGと表記)という仕組みを使う必要があるからです。

    RDBでは複数行であっても、異なるテーブル間であっても簡単にまとめて更新できますが、データストアではEGという単位でしか複数エンティティの一括更新が行えません。

    EGとはKeyに親子関係を持つエンティティの集まりのことで、Keyを生成する際に親となるKeyを指定することで、それらのKeyは親子関係になり、EGが形成されます。データストアはこのEG単位の更新についてACID性を保障しています。

    今回は、HeadのKeyを”親キー”としてBodyのキーに含めることでEGを形成しています。

    親キー KIND idまたはname キーの値
    headの主キー情報 Head 1 Head(1)
    bodyの主キー情報 Head(1) Body 1 Head(1)/Body(1)

    こうすることで、Head、Bodyを1トランザクションでまとめて更新することができるようになります。ちなみに、Headのように親キーが指定されていないエンティティをルートのエンティティと呼びます。

    余談ですが、実はSlim3ではこの”EG単位でしかまとめて更新できない制約”を乗り越える GlobalTransaction
    という強力な機能がありますが、それはまた別の機会に紹介します。

    1-2)リレーションシップの設定

    HeadにBodyを関連付けします。

    head.getBodyRef().setModel(body);

    以上でデータストア保存の前準備は完了です。

    2)登録処理

    Head、BodyはEGなので1つのトランザクションでまとめて更新することができます。EGを更新する際は、トランザクションの開始、終了(コミット、ロールバック)を明示的に指示します。

    Transaction tx = Datastore.beginTransaction();
    try {
        Datastore.put(tx, head, body);
        Datastore.commit(tx);
    }
    catch (Exception e) {
        if (tx.isActive()) {
            Datastore.rollback(tx);
        }
        throw e;
    }

    以上でBbsService#insert()の実装は完了です。コードを保存してテストを実行して下さい。

  • テストの実施結果がグリーンになれば「記事の新規登録」処理は完成です。