備忘録

働きたくないでござる

Elixir の Umbrella プロジェクトで Phoenix アプリケーションを作成 (4)

前回: giraphme.hatenablog.com

前回は Phoenix アプリケーションの追加と Cowboy サーバー起動までをやったので、今日は REST API を作成していきます。

構成

恒例の構成を再掲。

$ tree .
.
├── apps
│   ├── us_core # DB関連のビジネスロジックをもった Elixir アプリケーション
│   └── us_api # REST API を提供する Phoenix アプリケーション ← イマココ
│   └── us_web # React を利用した View を提供する Phoenix アプリケーション
│   └── us_admin_web # 管理画面を提供する Phoenix アプリケーション

API Endpoints

今回は読み込み系2つのみの作成です。(Rails の resources 的には index と show )

GET /api/v1/articles

OUT

{
  "articles": [
    {
      "id": 1234,
      "title": "EMT",
      "body": "エミリアたんマジ天使",
      "created_at": "2017-09-21 00:00:00",
      "updated_at": "2017-09-21 00:00:00",
    }
  ]
}

GET /api/v1/articles/:id

OUT

{
  "article": {
    "id": 1234,
    "title": "EMT",
    "body": "エミリアたんマジ天使",
    "created_at": "2017-09-21 00:00:00",
    "updated_at": "2017-09-21 00:00:00",
  }
}

ジェネレーターコマンド

コマンドで一通りのファイルを作成することができますが、今回はモデルは不要で Controller と View だけが必要です。 以下のコマンドを実行してヘルプを見てみましょう。

$ mix help phx.gen.json

                                mix phx.gen.json

Generates controller, views, and context for an JSON resource.

    mix phx.gen.json Accounts User users name:string age:integer

...

## Generating without a schema or context file

In some cases, you may wish to bootstrap JSON views, controllers, and
controller tests, but leave internal implementation of the context or schema to
yourself. You can use the --no-context and --no-schema flags for file
generation control.

...

抜粋しましたが、 --no-context --no-schema オプションを付けると良さそうですね。 というわけで私の場合はこんなコマンドを実行しました。

$ mix phx.gen.json UsCore Article articles  --no-context --no-schema

コマンド実行後 apps/us_api/lib/us_api_web/* などにジェネレートされます。
なお、私のプロジェクトの場合は us_api_web を消してしまった関係で、それぞれ手動でファイル移動する必要がありました。(出力先のパスを指定するオプションは存在しないっぽい)

それと、ジェネレートされたファイル内のモデル呼び出しやモジュール名(UsApiWeb など)は適宜変更してください。

Controller, Routing

今回は以下のようにコントローラーとルーティングを定義しました。

apps/us_api/lib/controllers/article_controller.ex

defmodule UsApi.ArticleController do
  use UsApi, :controller

  action_fallback UsApi.FallbackController

  def index(conn, _params) do
    articles = UsCore.list_articles()
    render(conn, "index.json", articles: articles)
  end

  def show(conn, %{"id" => id}) do
    article = UsCore.get_article!(id)
    render(conn, "show.json", article: article)
  end
end

apps/us_api/lib/router.ex

defmodule UsApi.Router do
  use UsApi, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api/v1", UsApi do
    pipe_through :api

    resources "/articles", ArticleController, only: [:index, :show]
  end
end

UsCore のインターフェイス

今回、 UsApi 等外部サービスへのインターフェイスapps/us_core/lib/us_core.ex にまとめる予定です。
どう考えても肥大化して Fat になるので、分散方法については後日運用しながら記事にしたいと思います。

apps/us_core/lib/us_core.ex

defmodule UsCore do
  import Ecto.Query
  import UsCore.Repo

  alias UsCore.Article

  def get_article!(id) do
    Article |> get(id)
  end

  def list_articles() do
    Article |> all
  end
end

アクセスしてみる

とりあえずアクセスできる状態になったはずなので、アクセスしてみましょう。

$ curl http://0.0.0.0:4000/api/v1/articles | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    19  100    19    0     0   1067      0 --:--:-- --:--:-- --:--:--  1117
{
  "data": [
    {
      "id": 1
    }
  ]
}

希望するレスポンス構造になっていませんが、リクエストの発行とレスポンスの取得に成功しました。

レスポンス構造の定義

これまでずっと Rails を触ってきたので、 json.jbuilder 的な DSL 記法をできそうなファイルを探していたのですが、Phoenix ではまじめにモジュールで書く必要があるようです。
Partial が各所にばらまかれている jbuilder に比べて、個人的にはこっちのほうが好みだなと思いました。

apps/us_api/lib/views/article_view.ex

defmodule UsApi.ArticleView do
  use UsApi, :view
  alias UsApi.ArticleView

  def render("index.json", %{articles: articles}) do
    %{articles: render_many(articles, ArticleView, "article.json")}
  end

  def render("show.json", %{article: article}) do
    %{article: render_one(article, ArticleView, "article.json")}
  end

  def render("article.json", %{article: article}) do
    %{
      id: article.id,
      title: article.title,
      body: article.body,
      created_at: article.created_at,
      updated_at: article.updated_at,
    }
  end
end
$ curl http://0.0.0.0:4000/api/v1/articles | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   161  100   161    0     0   2012      0 --:--:-- --:--:-- --:--:--  2037
{
  "articles": [
    {
      "updated_at": "2017-09-28T15:14:59.000000",
      "title": "EMT",
      "id": 1,
      "created_at": "2017-09-28T15:14:59.000000",
      "body": "エミリアたんマジ天使"
    }
  ]
}
$ curl http://0.0.0.0:4000/api/v1/articles/1 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   158  100   158    0     0   4327      0 --:--:-- --:--:-- --:--:--  4388
{
  "article": {
    "updated_at": "2017-09-28T15:14:59.000000",
    "title": "EMT",
    "id": 1,
    "created_at": "2017-09-28T15:14:59.000000",
    "body": "エミリアたんマジ天使"
  }
}

なんだか雑なコードなので不安はありますが、要件は満たしているので今日はこれぐらいにしといてやろう......。
余談ですが、レコードを大量に作って LIMIT をつけて速度を見てみた感じ、100件で 3ms, 1,000件で 20ms, 5,000件で 160ms, 10,000件で 376ms のレスポンスタイムでした。
実際のプロジェクトでこれほど単純なデータを扱うことは中々ないですが、すごく速いですね。

以上です。