はじめに

こんにちは。西日本テクノロジー&イノベーション室の齊藤です。

私の所属する西日本テクノロジー&イノベーション室では、2020年9月末に「SPA + REST API構成のサービス開発リファレンス(以下サービス開発リファレンス)」を公開しました。こちらは、シングルページアプリケーション(以下SPA)とREST APIから構成されるWebアプリケーションを開発する際に活用して頂けるコンテンツになっています。

前回のブログでは、SPAに絞ってタスク管理をするための簡易的な画面(Todo画面)の実装をご紹介しました。
今回はTodo一覧取得のREST APIを作り、前回作成したTodo画面とつなぎこんでみます。

  1. サービス開発リファレンスを使ってWebアプリケーションを作成してみよう<導入編>
  2. サービス開発リファレンスを使って1画面作成してみよう
  3. サービス開発リファレンスを使ってREST APIを1つ作成してみよう(←今回の記事)

    事前準備

    1~2までの記事でご紹介した作業の完成状態をベースに作成しますので、こちらの作業がまだの方は実施してみてください。

    バックエンド(REST API)の作成

    前回は以下のようなTodo画面を作成しました。

    ここで表示している洗濯物を干す部屋を掃除するというタスクは、ソースコードにハードコーディングしています。
    今回はデータベースからタスクを取得するREST APIを作成し、REST APIから取得した結果をTodo画面に表示するようにします。

    データベース周りの処理の実装

    データベースには、PostgreSQLを使用します。

    マイグレーションツールであるFlywayを使用して、データベース上のテーブルやテストデータを登録するようにします。
    Flywayを使うことでデータベースの状態をバージョン管理でき、データベースに適用されているSQLを管理できたり、サーバを起動してすぐにアプリケーションを試せる状態になっていて素早く開発できるなどのメリットがあります。

    Flywayについては、以下記事によくまとめられていますので、興味がある人は読んでみてください。
    参考:Flyway使い方メモ

    Flywayの実行については、Nablarchの初期化処理を利用して、バックエンドアプリを起動したタイミングで行うよう設定します(参考:Nablarch – オブジェクトの初期化処理を行う

    backend/src/main/resources/rest-component-configuration.xml の以下コメントアウトを外します。

    <component-ref name="dbMigration" />

    次に、Todoの内容を扱うTODOテーブルを作成します。backend/src/main/resources/db/migration パッケージを作成し、その配下にV1__create_table.sql を作成します。ファイルには、TODOテーブルを作成するためのSQLを記述します。

    backend/src/main/resources/db/migration/V1__create_table.sql

    CREATE TABLE TODO
    (
        TODO_ID   BIGINT      NOT NULL,
        TEXT      VARCHAR(20) NOT NULL,
        PRIMARY KEY (TODO_ID)
    );

    また、テスト実行時にデータベースにテストデータを投入するため、テスト用リソースディレクトリの backend/src/test/resources に、db/testdataディレクトリを作成し、V9999__testdata.sqlファイルを作成します。ファイルには、テストデータを登録するためのSQLを記述します。

    backend/src/test/resources/db/testdata/V9999__testdata.sql

    INSERT INTO TODO (TODO_ID, TEXT) VALUES (1, 'やること1');
    INSERT INTO TODO (TODO_ID, TEXT) VALUES (2, 'やること2');
    INSERT INTO TODO (TODO_ID, TEXT) VALUES (3, 'やること3');

    Nablarchでは、データベースを操作するためにユニバーサルDAOという機能を使用します。

    ユニバーサルDAOでは、エンティティと呼ばれるクラスを使用して、データベースの検索結果をマッピングします。
    TODOテーブルは先ほど作成しているため、TODOテーブルに対応するTodoEntityクラスを作成します。

    backend/src/main/java/com/example/infrastructure/persistence/entity/TodoEntity.java

    package com.example.infrastructure.persistence.entity;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "todo")
    @Access(AccessType.FIELD)
    public class TodoEntity {
    
        @Id
        private Long todoId;
    
        private String text;
    
        public Long getTodoId() {
            return todoId;
        }
    
        public void setTodoId(Long todoId) {
            this.todoId = todoId;
        }
    
        public String getText() {
            return text;
        }
    
        public void setText(String text) {
            this.text = text;
        }
    }

    これでデータベース周りの準備は完了しました。

    アクションクラスの実装

    次に、登録しているToDoを取得するためのREST APIを実装するTodosActionを作成します。

    backend/src/main/java/com/example/presentation/restapi/todo/TodosAction.java

    package com.example.presentation.restapi.todo;
    
    import com.example.infrastructure.persistence.entity.TodoEntity;
    import nablarch.common.dao.EntityList;
    import nablarch.common.dao.UniversalDao;
    import nablarch.core.repository.di.config.externalize.annotation.SystemRepositoryComponent;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.core.MediaType;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @SystemRepositoryComponent
    @Path("/todos")
    public class TodosAction {
    
        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public List<TodoResponse> get() {
            EntityList<TodoEntity> todoEntities = UniversalDao.findAll(TodoEntity.class);
    
            return todoEntities.stream()
                    .map(entity -> new TodoResponse(entity.getTodoId(), entity.getText()))
                    .collect(Collectors.toList());
        }
    
        public static class TodoResponse {
    
            public final Long id;
    
            public final String text;
    
            public TodoResponse(Long id, String text) {
                this.id = id;
                this.text = text;
            }
        }
    }

    レスポンスのオブジェクトがJSONに変換されるため、TodoResponseクラスを内部で定義し、データベースから取得したオブジェクトを変換します。また、java.util.Listで返すことで配列形式に変換されるため、TodoResponse型のオブジェクトをListで返すことで、次のようなJSONに変換されます。

    {
      [
        {
          "id": 1,
          "text": "やること1",
        },
        {
          "id": 2,
          "text": "やること2",
        },
        {
          "id": 3,
          "text": "やること3",
        }
      ]
    }

    テストの実装

    アクションクラスを作成してREST APIを定義したため、このREST APIが想定している動作をするのかテストします。

    Nablarchでは、REST APIをテストするためテスティングフレームワークを提供していますので、それを使用してアプリサーバの起動およびREST APIの呼び出しをテストします。

    backend/src/test/resources/unit-test.xml の以下をコメントアウトしてください。

    <component class="com.example.presentation.restapi.CsrfTokenManager" />

    次に、テスト用Javaディレクトリのsrc/test/javaに、com.example.todoパッケージを作成し、そこにRestApiTestクラスを作成します。

    backend/src/test/java/com/example/todo/RestApiTest.java

    package com.example.todo;
    
    import nablarch.fw.web.HttpResponse;
    import nablarch.fw.web.RestMockHttpRequest;
    import nablarch.test.core.http.SimpleRestTestSupport;
    import org.junit.Test;
    
    import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.hamcrest.Matchers.equalTo;
    import static org.hamcrest.Matchers.hasSize;
    
    public class RestApiTest extends SimpleRestTestSupport {
    
        @Test
        public void RESTAPIでToDo一覧が取得できる() {
            RestMockHttpRequest request = get("/api/todos");
            HttpResponse response = sendRequest(request);
    
            assertStatusCode("ToDo一覧の取得", HttpResponse.Status.OK, response);
    
            String responseBody = response.getBodyString();
    
            assertThat(responseBody, hasJsonPath("$", hasSize(3)));
    
            assertThat(responseBody, hasJsonPath("$[0].id", equalTo(1)));
            assertThat(responseBody, hasJsonPath("$[0].text", equalTo("やること1")));
    
            assertThat(responseBody, hasJsonPath("$[1].id", equalTo(2)));
            assertThat(responseBody, hasJsonPath("$[1].text", equalTo("やること2")));
    
            assertThat(responseBody, hasJsonPath("$[2].id", equalTo(3)));
            assertThat(responseBody, hasJsonPath("$[2].text", equalTo("やること3")));
        }
    }

    REST APIのテスティングフレームワークを使用するため、親クラスにnablarch.test.core.http.SimpleRestTestSupportクラスを指定します。

    合わせて、最初のテストとしてREST APIが想定しているパスとHTTPメソッドで呼び出せるかをテストします。

    また、json-path-assertというライブラリを使用して、レスポンスとして返されたJSONに対して次の検証をします(JsonPathの記法について詳しくはドキュメントを参照してください)

    • 配列の要素数が3つである
    • 配列の1番目が、データの1番目の値と同じである
    • 配列の2番目が、データの2番目と値と同じである
    • 配列の3番目が、データの3番目と値と同じである

    ここまで実装できたら、動作確認をしてみましょう。

    以下コマンドでデータベースを起動します。

    $ docker-compose up -d

    次のようにdocker-compose psコマンドを実行してStateUpになっていれば起動成功です。

    $ docker-compose ps
    Name                 Command                        State     Ports                     
    -----------------------------------------------------------------------------------------------------------------
    example_postgres_1   docker-entrypoint.sh postgres    Up      0.0.0.0:5432->5432/tcp  
    example_redis_1      docker-entrypoint.sh redis ...   Up      0.0.0.0:6379->6379/tcp

    次に、backendディレクトリで以下コマンドを実行しましょう。

    $ mvn test

    BUILD SUCCESS となっていれば成功です。

    [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.762 s - in com.example.todo.RestApiTest
    [INFO] 
    [INFO] Results:
    [INFO] 
    [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
    [INFO] 
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------

    フロントエンド(SPA)とのつなぎこみ

    これでTodoを取得するREST APIの実装が完了しましたので、前回作成したTodo画面とつなぎこんでみましょう。

    backendディレクトリで以下コマンドを実行して、バックエンドのアプリを起動しておきます。

    $ mvn jetty:run

    次に、SPA側で今回作成したAPIを呼び出す処理を実装します。
    BackendService.tsに以下を追加してください。

    frontend/src/example/backend/BackendService.ts

    const getTodos = async () => {
      Logger.debug('call service of getTodos');
    
      const response = await restClient.get('/api/todos');
      Logger.debug(response);
      return response.json();
    };

    また、export defaultに上記で宣言したgetTodosを追加します。

    export default {
      getHelloWorld,
      getTodos,
    };

    ここまで実装すると、BackendService.tsは次のようになっています。

    import { RestClient, Logger } from 'framework';
    
    const restClient = new RestClient();
    
    const getHelloWorld = async () => {
      Logger.debug('call service of getHelloWorld');
    
      const response = await restClient.get('/api/helloWorld');
      Logger.debug(response);
      return response.json();
    };
    
    const getTodos = async () => {
      Logger.debug('call service of getTodos');
    
      const response = await restClient.get('/api/todos');
      Logger.debug(response);
      return response.json();
    };
    
    export default {
      getHelloWorld,
      getTodos,
    };

    次に、Todo.tsxuseEffectで直にTodoを記載していた部分を以下のように修正し、REST APIからTodoの内容を取得するようにします。

    frontend/src/example/components/pages/Todo.tsx

    type Todo = {
      id: number
      text: string
    }
    
    ~~~~~~~~~
    
    useEffect(() => {
        BackendService.getTodos()
            .then(response => {
              Logger.debug(response);
              setTodos(response.map((todo: Todo) => todo.text));
            });
      }, []);

    ここまで実装すると、Todo.tsxは次のようになっています。

    import React, {useEffect, useState} from 'react';
    import { Logger, useInput } from "../../../framework";
    import { BackendService } from '../../backend';
    import './Todo.css';
    
    type Todo = {
      id: number
      text: string
    }
    
    const Todo: React.FC = () => {
      const [todos, setTodos] = useState<string[]>([]);
      const [text, textAttributes] = useInput('');
    
      useEffect(() => {
        BackendService.getTodos()
            .then(response => {
              Logger.debug(response);
              setTodos(response.map((todo: Todo) => todo.text));
            });
      }, []);
    
      const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        setTodos([...todos, text]);
      };
    
      return (
          <div className="content">
            <div className="form_field">
              <form className="form" onSubmit={handleSubmit}>
                <div className="input_field">
                  <input type="text" {...textAttributes} placeholder="やることを入力してください" />
                </div>
                <div className="button_field">
                  <button type="submit">追加</button>
                </div>
              </form>
            </div>
            <ul className="list">
              {todos.map((todo, index) =>
                  <li className="item" key={index}>
                    <div className="todo">
                      <span>{todo}</span>
                    </div>
                  </li>
              )}
            </ul>
          </div>
      );
    };
    
    export default Todo;

    これでSPA側の実装も完了したため、以下コマンドをfrontendディレクトリで実行してみましょう。

    $ npm start

    自動でブラウザが立ち上がり、Top画面が表示されているはずです。
    今回はTodo画面を実装したため、以下URLを開いてみましょう。
    http://localhost:3000/todo

    データベースに入っている内容が画面に表示されていれば成功です。

    まとめ

    今回は、サービス開発リファレンスを使ってTodoを取得するREST APIを実装しました。
    次回はバリデーションの機能をご紹介します。


    本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。