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

Serviceの作成 (その2)

「記事の取得(主キー検索)」「記事の上書き更新」「記事の削除」の実装

  • テストケースの作成今度は一気にRead、Update、Delete処理を実装して、記事のCRUDテストケースを完成させます。青字が手を加えた箇所になります。
    @Test
    public void crudTest() 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);
        // 新規作成したHeadと、登録後再取得したHeadが等しいこと 
        assertEqualsHead(head, storedHead);
        // 登録後のバージョンが1であること 
        assertEquals(storedHead.getVersion(), Long.valueOf(1L));
        // ========== assertion end ========== // 
    
        // ===================================== 
        // 記事の一件取得 
        // ===================================== 
        // 記事一覧から一つの記事が選択された想定で再取得 
        storedHead = service.get(storedHead.getKey());
        // ========== assertion start ========== // 
        // 指定されたKeyの記事が取得できていること 
        assertNotNull(storedHead);
        // 先に登録したHeadであること 
        assertEqualsHead(head, storedHead);
        // バージョンが1であること 
        assertEquals(storedHead.getVersion(), Long.valueOf(1L));
        // ========== assertion end ========== // 
    
        // ===================================== 
        // 記事の上書き更新 
        // ===================================== 
        // 記事の変更 
        storedHead.setSubject("修正した記事");
        Body storedBody = storedHead.getBodyRef().getModel();
        storedBody.setText("上書き更新した本文です。");
        // データストアへ上書き更新 
        service.update(storedHead, storedBody);
        // 更新後の記事一覧の取得 
        headList = service.getAll();
        // ========== assertion start ========== // 
        // 投稿後の記事一覧は1件であること 
        assertNotNull(headList);
        assertTrue(headList.size() == 1);
        // 以降、1件の中身のチェック 
        Head updatedHead = headList.get(0);
        // 修正したHeadと、更新後再取得したHeadが等しいこと 
        assertEqualsHead(storedHead, updatedHead);
        // 上書き更新したのでバージョンが2になっていること 
        assertEquals(updatedHead.getVersion(), Long.valueOf(2L));
        // ========== assertion end ========== // 
    
        // ===================================== 
        // 記事の削除 
        // ===================================== 
        // 記事の削除 
        service.delete(updatedHead.getKey());
        // 削除後の記事の取得 
        storedHead = service.get(updatedHead.getKey());
        // 削除後の記事一覧の取得 
        headList   = service.getAll();
        // ========== assertion start ========== // 
        // この記事は削除されているのでNullであること 
        assertNull(storedHead);
        // 記事一覧は0件のリストであること 
        assertNotNull(headList);
        assertTrue(headList.isEmpty());
        // ========== assertion end ========== // 
    }
     
    private void assertEqualsHead(Head head1, Head head2) {
        // 共にNullではないこと 
        assertNotNull(head1);
        assertNotNull(head2);
        // 記事タイトル、ユーザ名、投稿日時が等しいこと 
        assertEquals(head1.getSubject(), head2.getSubject());
        assertEquals(head1.getUsername(), head2.getUsername());
        assertEquals(head1.getPostDate(), head2.getPostDate());
        // 共にリレーションシップからBodyが取得できること 
        Body body1 = head1.getBodyRef().getModel();
        Body body2 = head2.getBodyRef().getModel();
        assertNotNull(body1);
        assertNotNull(body2);
        // 本文が等しいこと 
        assertEquals(body1.getText(), body2.getText());
    }

    コメントを沢山書いたので、テストの要約は省きます。例によってこのテストを実施するために、以下のメソッドを実装します。

    • BbsService#get(Key)
    • BbsService#update(Head, Body)
    • BbsService#delete(Key)
  • 必要な処理の実装コードは以下になります。
    public Head get(Key headKey) {
        return Datastore.getOrNull(HeadMeta.get(), headKey);
    }
    
    public void update(Head head, Body body) throws Exception {
        Transaction tx = Datastore.beginTransaction();
        try {
            Datastore.get(tx, HeadMeta.get(), head.getKey(), head.getVersion());
            Datastore.put(tx, head, body);
            Datastore.commit(tx);
        }
        catch (Exception e) {
            if (tx.isActive()) {
                Datastore.rollback(tx);
            }
            throw e;
        }
    }
    
    public void delete(Key headKey) throws Exception {
        Transaction tx = Datastore.beginTransaction();
        try {
            Head head = Datastore.get(tx, HeadMeta.get(), headKey);
            Key bodyKey = head.getBodyRef().getKey();
            Datastore.delete(tx, headKey, bodyKey);
            Datastore.commit(tx);
        }
        catch (Exception e) {
            if (tx.isActive()) {
                Datastore.rollback(tx);
            }
            throw e;
        }
    }

    順にロジックを説明します。

    • 「記事の取得(主キー検索)」について
      public Head get(Key headKey) {
          return Datastore.getOrNull(HeadMeta.get(), headKey);
      }

      主キーを用いてデータストアからHeadを取得しています。データストア検索において、Keyを使った検索は最も高速かつ低コストな方法です。

      Datastore.getOrNull() は、指定されたKeyに該当するエンティティがあればそれを返し、無ければ
      null を返します。

      似たメソッドに、Datastore.get()がありますが、こちらは該当するエンティティが無い場合、
      EntityNotFoundRuntimeException がスローされます。

    • 「記事の上書き更新」について
      public void update(Head head, Body body) throws Exception {
          Transaction tx = Datastore.beginTransaction();
          try {
              Datastore.get(tx, HeadMeta.get(), head.getKey(), head.getVersion());
              Datastore.put(tx, head, body);
              Datastore.commit(tx);
          }
          catch (Exception e) {
              if (tx.isActive()) {
                  Datastore.rollback(tx);
              }
              throw e;
          }
      }

      既存データに対する上書き更新は以下の流れで行います。

      1)トランザクションを開始する

      2)更新しようとしているデータが、他者によって更新されていないかチェックする

      3)データを更新する

      4)トランザクションをコミットする

      例外処理

      2~4)のどこかで失敗した場合はトランザクションをロールバックする

      データストアの更新は楽観的排他制御で行います。
      そこでポイントとなるのが 2)に該当する以下のコードです。

      Datastore.get(tx, HeadMeta.get(), head.getKey(), head.getVersion());

      Datastore.get()はデータストアから指定されたKeyに該当するエンティティを取得するメソッドですが、併せてversionを渡すことで、他者によって更新されていないかをチェックしてくれます。(つまり楽観的排他制御をしてくれます)

      ここでもし、データストアの最新versionと不一致、つまり他者がこのエンティティを更新していた場合は、ConcurrentModificationException をスローします。

      このversionによる追い越し更新チェックの後、実際の更新処理に入りますが、実はもう一つ考慮する箇所があります。それは、前述のDatastore.get() から Datastore.put() までの僅かの間に、やはり他者に割り込み更新される可能性があることです。

      そこで、トランザクションを使います。

      Transaction tx = Datastore.beginTransaction();
      try {
          Datastore.get(tx, HeadMeta.get(), head.getKey(), head.getVersion());
          Datastore.put(tx, head, body);
          Datastore.commit(tx);
      }

      Datastore.get()に、トランザクション(tx)を引数に指定することで、その時点からこのトランザクションをコミットするまでの間に、他者に割り込み更新されていなければ、このトランザクションは全て実行され、もし割り込み更新されていた場合は、このトランザクションは全て失敗します。

      つまり、get()→put()→commit()までに割り込み更新がなければ、Head、Bodyともに更新され、割り込み更新があればHead、Bodyともに更新失敗となります。

      トランザクションが失敗するとConcurrentModificationExceptionがスローされるので、ロールバックを行います。

      catch (Exception e) {
          if (tx.isActive()) {
              Datastore.rollback(tx);
          }
          throw e;
      }
    • 「記事の削除」について
      public void delete(Key headKey) throws Exception {
          Transaction tx = Datastore.beginTransaction();
          try {
              Head head = Datastore.get(tx, HeadMeta.get(), headKey);
              Key bodyKey = head.getBodyRef().getKey();
              Datastore.delete(tx, headKey, bodyKey);
              Datastore.commit(tx);
          }
          catch (Exception e) {
              if (tx.isActive()) {
                  Datastore.rollback(tx);
              }
              throw e;
          }
      }

      記事の削除も、上書き更新とほぼ同じ流れですが version による楽観的排他制御をしていません。

      Datastore.get(tx, HeadMeta.get(), headKey);

      今回はあえて追い越し更新に関係なく削除するようにしています。アプリの要件によっては削除処理も楽観的排他制御を使う場合も十分あり得るでしょう。

    以上で「記事の取得(主キー検索)」「記事の上書き更新」「記事の削除」処理の実装は完了です。コードを保存してテストを実行して下さい。

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