The HIRO Says

If you smell what The HIRO is cooking!!!

【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);
    }
}