初心者がElixir Phoenix + MySQL でちゃんと公開できるJSON REST APIサーバーを作成するまで (Sana API Server)

WEB+DB PRESS vol88&vol89のElixirの紹介が言語紹介の文章でいまだかつてないほどのわかり易さだったので
Elixirド素人がElixirとPhoenixフレームワークを使ってちゃんと公開できるREST APIサーバーを作ってみるまでを紹介します。
これを読めばたぶん２時間ぐらいで任意のデータのREST APIサーバーが作成できます。

[フル版のソース - github](https://github.com/Project-ShangriLa/sana_server_phoenix/tree/v1.4.2-Release1.2)

実際に作成したAPIの仕様
[放映中のアニメ公式 Twitterアカウントのフォロワー変動履歴情報を提供するRESTful API サーバーを作りました](http://qiita.com/AKB428/items/c12c70b16c924a45f73f)

## 作成テーマ要件
- Elixir PhoenixフレームワークでREST API サーバーを作る
- 今回扱うデータは特定分野のTwitterアカウントリストのフォロワー履歴とする
- MySQLに格納されたデータをREST API経由で取得できるようにする
- インターネットにサーバーと仕様を公開するためセキュリティに注意する

 
## 詳細仕様
- CRUDのRのみを提供（更新はしない、テーブル更新はバッチでシステムが行う想定）
- 最終的にNginxと連携し、フロントの入り口はNginxにする
- Railsからのシステムマイグレーションを想定し、テーブルはRailsで作られた想定で作成する


## テーブル構成

![SanaAPI　DB.png](https://qiita-image-store.s3.amazonaws.com/0/36931/39f55f23-00ed-cb4e-5ec2-46aa63d7b777.png)

basesテーブルのidで各テーブルを紐付けます

|テーブル| 取り扱うデータ|
|----|----|
|bases| アニメ作品のマスターデータ。作品名やTwitterアカウント|
|twitter_statuses|Twitterアカウントの現在のフォロワー数|
|twitter_status_histories|Twitterアカウントのフォロワー数の履歴|



詳細なDDLはこちら [shangrila-DDL](https://github.com/Project-ShangriLa/shangrila/tree/master/DDL)

## API仕様

|エンドポイント|内容|
|----|----|
|/anime/v1/twitter/follower/status|twitter_statusesの情報をJSONで返す|
|/anime/v1/twitter/follower/history|twitter_status_historiesの情報をJSON返す|

簡単ですね
１テーブルに１エンドポイントが紐づく形にします

## Elixirのインストール

### OSX

```
homebrew install elixir
```

### Linux

https://www.erlang-solutions.com/downloads/download-erlang-otp
上記のPRMからErlangを入れてElixirソースコードをコンパイルします

```
git clone https://github.com/elixir-lang/elixir.git
cd elixir/
git checkout v1.1.1
make clean test
```

```
export PATH="$PATH:/path/to/elixir/bin"
```

Linuxの場合はdnfコマンドで入れる方法もあるようです。
Elixirが入るとライブラリ依存ツールのmixコマンドも使えるようになります。（rubyのbundler相当のコマンド）

## エディタ

### Vim

https://github.com/elixir-lang/vim-elixir

![スクリーンショット 2015-11-15 1.55.12.png](https://qiita-image-store.s3.amazonaws.com/0/36931/604433d7-f002-b59a-9ca8-14fc9f4c110f.png)

その他、githubのElixirプロジェクトページにはEmacsとSublime Textのプラグインがあります。

### Atom

![スクリーンショット 2015-11-15 1.55.23.png](https://qiita-image-store.s3.amazonaws.com/0/36931/7e04617e-d867-0321-a314-65af7adcd821.png)

https://atom.io/packages/language-elixir


## プロジェクトの作成

mixコマンドで作成

```
mix phoenix.new sana_server_phoenix --database mysql --no-brunch
cd sana_server_phoenix
mix phoenix.server
```

PhoenixフレームワークはデフォルトのデータベースをPostgreSQLにしているため
MySQL用にプロジェクトを作成する場合はオプション指定が必要です。

localhost:4000にアクセスすると起動が確認できます

![スクリーンショット 2015-11-14 17.44.40.png](https://qiita-image-store.s3.amazonaws.com/0/36931/8e762fb1-cebe-9a2c-bef8-12fb600c5ca9.png)

mix phoenix.serverでサーバーを起動します。
コードの変更は自動的に反映されるのでコードを更新して反映する必要はないです(ruby rerun相当の機構が最初から入っている)
ただし、設定ファイル系の更新は反映されないので、手動更新が必要です。

phoenixのmix コメントタスク一覧は以下
http://www.phoenixframework.org/docs/mix-tasks



## 依存するライブラリを指定

ログファイルを扱うため**logger_file_backend**と日時のフォーマットを指定するため**timex**を追加します

mix.exsはrubyでいうところのGemfileです


```ex:mix.exs
  defp deps do
    [{:phoenix, "~> 1.0.3"},
     {:phoenix_ecto, "~> 1.1"},
     {:mariaex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:cowboy, "~> 1.0"},
     {:timex, "~> 0.9"},
     {:logger_file_backend, "~> 0.0.5"}]
  end
```

欲しいモジュールは[Hex](https://hex.pm/)から探します。但しまだ数は少ない、発展途上。
Hexはモジュールを集めたサーバーで[Rubygems](https://rubygems.org/)のようなサイト。

### 依存するライブラリをダウンロード

```
mix deps.get
```


## データベース接続設定

MySQLサーバーへの接続設定を記入します

```ex:config/dev.exs
# Configure your database
config :sana_server_phoenix, SanaServerPhoenix.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "root",
  password: "",
  database: "anime_admin_development",
  hostname: "localhost",
  pool_size: 10
```

config/prod.exsやconfig/test.exsも必要に応じて設定します



## ルーティングの設定

web/router.ex に　以下を追加します

```ex:web/router.ex

  scope "/", SanaServerPhoenix do
    pipe_through :api
    resources "/anime/v1/twitter/follower/status", TwitterFollowerStatusController, only: [:index]
    resources "/anime/v1/twitter/follower/history", TwitterFollowerHistoryController, only: [:index]
  end

```

注意しないといけないのは[:index]を追加する事です。
指定しない場合CRUDの全てのルーティングが有効化されるので一般公開するサーバーは危険です

コントローラーを実装した後に以下のコマンドで現在のルーティングは確認できます

```
sana_server_phoenix (master)$ mix phoenix.routes
```

```
                    page_path  GET  /                                   SanaServerPhoenix.PageController :index
 twitter_follower_status_path  GET  /anime/v1/twitter/follower/status   SanaServerPhoenix.TwitterFollowerStatusController :index
twitter_follower_history_path  GET  /anime/v1/twitter/follower/history  SanaServerPhoenix.TwitterFollowerHistoryController :index
```


## コントローラーの実装

### twitter_follower_status_controller.ex

```ex:web/controllers/twitter_follower_status_controller.ex
defmodule SanaServerPhoenix.TwitterFollowerStatusController do
  use SanaServerPhoenix.Web, :controller

  def index(conn, _params) do

    #GETパラメーターを取得
    account_list = String.split(_params["accounts"],",")

    #IN句のプリペアードステートメントを作成 引数が５だったら [?, ?, ?, ?, ?] 
    prepared_statement_list = []
    prepared_statement_list = Enum.map(account_list, fn(x) ->
      prepared_statement_list = prepared_statement_list ++ ["?"]
    end)

    prepared_statement_in =  Enum.join(prepared_statement_list, ",")

    {:ok, twitter_status } = Ecto.Adapters.SQL.query(Repo,
      "SELECT b.twitter_account, follower, updated_at
      from twitter_statuses, (
       SELECT id, twitter_account FROM bases where twitter_account IN (#{prepared_statement_in})
      ) b
      where twitter_statuses.bases_id = b.id order by updated_at desc", account_list)

    dict = HashDict.new
    dict = List.foldr(twitter_status[:rows], dict,
    fn (x, acc) ->
      [twitter_account, follower, updated_at] = x
      check = HashDict.put(acc, twitter_account, %{:follower => follower, :updated_at => UnixTime.convert_date_to_unixtime(updated_at)})
    end)

    #変数dump
    #IO.inspect dict

    render conn, msg: dict
  end

end


```

コードを解説する前にコードを理解しやすいように実行してみます

### 実行例

```
curl http://localhost:4000/anime/v1/twitter/follower/status?accounts=usagi_anime,kinmosa_anime | jq .
```

accountsパラメーターにTwitterアカウントをカンマ区切りで渡します

### 実行結果

```
{
  "usagi_anime": {
    "updated_at": 1411466007,
    "follower": 51345
  },
  "kinmosa_anime": {
    "updated_at": 1432364961,
    "follower": 57350
  }
}
```

### コード解説

```ex
  account_list = String.split(_params["accounts"],",")
```

#### GETパラメーターの取得

_params["accounts"]でGETパラメーターを取得します。
ここではカンマ区切りにして配列を生成しています。

#### Ectoで直接 生SQLを実行する

EctoはPhoenixに用意されているO/Rマッパー
Ecto.Adapters.SQL.queryで直接SQLを実行できる。

```
{:ok, SQLの結果 } = Ecto.Adapters.SQL.query(Repo, "SQLクエリ", bindパラメーターの配列)
```

ちなみにEctoのマニュアルにはバインドパラメーターを使いたい場合の文章サンプルが

```
Ecto.Adapters.SQL.query(Repo, "select * from from table where id = $1 and name = $2;", [param1, param2])
```

みたいになって混乱するが＄でなく普通に？をつかってバインドパラメーターを指定する

```
Ecto.Adapters.SQL.query(Repo, "select * from from table where id = ? and name = ?;", [param1, param2])
```

#### SQLの結果を加工する

SQLの結果は以下のような配列に格納されます

![SanaDB2.png](https://qiita-image-store.s3.amazonaws.com/0/36931/d1a26ba6-a8d5-347b-edb8-06068d045c25.png)

この配列をtwitter_accountをキーとしたハッシュ構造に変換します

```json
{
  "usagi_anime": {
    "updated_at": 1411466007,
    "follower": 51345
  },
  "kinmosa_anime": {
    "updated_at": 1432364961,
    "follower": 57350
  }
}
```



```ex
    dict = HashDict.new
    dict = List.foldr(twitter_status[:rows], dict,
    fn (x, acc) ->
      [twitter_account, follower, updated_at] = x
      check = HashDict.put(acc, twitter_account, %{:follower => follower, :updated_at => UnixTime.convert_date_to_unixtime(updated_at)})
    end)
```

-  HashDict.newでハッシュを作成
-  twitter_status[:rows]でSQLの結果を格納。twitter_statusにはSQL構文など結果以外の情報もが格納されている
-  ``List.foldr(twitter_status[:rows], dict,`` ・・第一引数にSQL結果配列、第二引数に格納対象のハッシュを指定


```ex
 [twitter_account, follower, updated_at] = x
```

これはElixirの構文で右辺の式を左辺の変数に分解する


##### ハッシュ構造にデータを追加

```
HashDict.put(格納先のハッシュ, （追加する）ハッシュのキー, （追加する）ハッシュの値)
```

```ex
 HashDict.put(acc, twitter_account, %{:follower => follower, :updated_at => UnixTime.convert_date_to_unixtime(updated_at)})
```
このコードでハッシュにデータが追加される。
%{:A => B} はハッシュを表す。


##### Elixirの変数は不変

``` dict = List.foldr(twitter_status[:rows], dict, ```

dictに値を返しているのは、Elixirの変数が不変なためHashDict.putでハッシュを追加してもオリジナルの変数は値が変更されません。
ここらへんはElixirの``IO.inspect``を用いて変数のダンプを追いかけると理解しやすいでしょう。


### twitter_follower_history_controller.ex

```ex:web/controllers/twitter_follower_history_controller.ex
defmodule SanaServerPhoenix.TwitterFollowerHistoryController do
  use SanaServerPhoenix.Web, :controller
  require Logger

  def index(conn, _params) do
    response = []

    #IO.inspect Logger.metadata()

    account = _params["account"]
    param_end_date = _params["end_date"]

    #ElixirではIFをパターンマッチで行う
    end_date = try do
      case param_end_date do
        nil -> DateUtil.now_format
        _ -> UnixTime.convert_unixtime_to_date(String.to_integer(param_end_date))
      end
    rescue
      e in ArgumentError -> e
      #Exception.messageで例外メッセージを取得
      Logger.warn "error param end_date. " <> Exception.message(e)
      DateUtil.now_format
    end

    {:ok, twitter_status } = Ecto.Adapters.SQL.query(Repo,
      "SELECT h.follower, h.updated_at
      from twitter_status_histories as h,
      (SELECT id FROM bases WHERE twitter_account = ? order by id desc limit 1) b
       where h.bases_id = b.id AND h.updated_at < ? order by h.updated_at desc limit ?",
       [account, end_date, 100])

    response = List.foldr(twitter_status[:rows], response,
    fn (x, acc) ->
      [follower, updated_at] = x
      response = [%{:follower => follower, :updated_at => UnixTime.convert_date_to_unixtime(updated_at)}] ++ acc
    end)

    render conn, msg: response
  end

end

#http://michal.muskala.eu/2015/07/30/unix-timestamps-in-elixir.html
defmodule UnixTime do
  #JSTなので9Hにしておく
  epoch = {{1970, 1, 1}, {9, 0, 0}}
  @epoch :calendar.datetime_to_gregorian_seconds(epoch)

  def convert_date_to_unixtime(created_at) do
    {{year, month, day}, {hour, minute, second, msec}} = created_at
    gs = :calendar.datetime_to_gregorian_seconds({{year, month, day}, {hour, minute, second}})
    gs - @epoch
  end

  def convert_unixtime_to_date(timestamp) do
    {:ok, date_string} =
    timestamp
      |> +(@epoch)
      |> :calendar.gregorian_seconds_to_datetime
      |> Timex.Date.from
      |> Timex.DateFormat.format("%Y-%m-%d %H:%M:%S", :strftime)
    date_string
  end

end

defmodule DateUtil do
  def now_format() do
    {:ok, date_string} =
      :erlang.localtime
      |> Timex.Date.from
      |> Timex.DateFormat.format("%Y-%m-%d %H:%M:%S", :strftime)
    date_string
  end
end

```


## viewの実装

コントローラーに対応するViewファイルを２つ用意します。
今回はコントローラーでデータ加工されたデータをJSONにして返すだけなので中身はほとんどありません。


``` web/views/twitter_follower_status_view.ex
defmodule SanaServerPhoenix.TwitterFollowerStatusView do
  use SanaServerPhoenix.Web, :view

  def render("index.json", %{msg: msg}) do
   msg
  end

end

```

``` web/views/twitter_follower_history_view.ex 
defmodule SanaServerPhoenix.TwitterFollowerHistoryView do
  use SanaServerPhoenix.Web, :view

  def render("index.json", %{msg: msg}) do
   msg
  end

end

```

## その他 アドバンス

### プロダクション起動

```
MIX_ENV=prod mix compile.protocols
MIX_ENV=prod PORT=4000 mix phoenix.server
```

### Nginxとの連携

UNIXソケットでなく普通にHTTPプロキシします

```nginx

server {
      listen       80;
      server_name api.moemoe.tokyo;
      location /anime/v1/twitter/ {
         proxy_pass http://localhost:4000;
      }
    }
```


### ログファイルの設定

このままだとコンソールにログが流れっぱなしになるのでconfigに設定を追加します

```config/config.exs
# Configures Elixir's Logger
config :logger, backends: [{LoggerFileBackend, :file}]
config :logger, :file,
  path: "./log/sana_api_phoenix.log",
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]
```

```
tail -f ./log/sana_api_phoenix.log


17:44:37.251 request_id=cgse5i65gge1u70cdf8flfsjbauo52d4 [info] Sent 200 in 90ms
17:44:37.611 request_id=k43krmuoii9pi3vmvqjskdm22fnucpqf [info] GET /apple-touch-icon-precomposed.png
17:44:37.692 request_id=i15kbm8qhjueesqeclro53mcm5jga2gl [info] GET /apple-touch-icon.png
18:19:38.998 request_id=k11tmlh81emuadp1cah2e26aenfrpm72 [info] GET /anime/v1/twitter/follower/status
18:19:38.999 request_id=k11tmlh81emuadp1cah2e26aenfrpm72 [debug] Processing by SanaServerPhoenix.TwitterFollowerStatusController.index/2
  Parameters: %{"accounts" => "usagi_anime,kinmosa_anime"}
  Pipelines: [:api]
18:19:39.078 request_id=k11tmlh81emuadp1cah2e26aenfrpm72 [debug] SELECT b.twitter_account, follower, updated_at
      from twitter_statuses, (
       SELECT id, twitter_account FROM bases where twitter_account IN (?,?)
      ) b
      where twitter_statuses.bases_id = b.id order by updated_at desc ["usagi_anime", "kinmosa_anime"] OK query=8.5ms queue=60.0ms
18:19:39.087 request_id=k11tmlh81emuadp1cah2e26aenfrpm72 [info] Sent 200 in 88ms
```

環境に関係なくログを残したいならconfig.exsを編集。
「devはコンソールログで、prodはログファイルにしたい」など環境ごとに分けたい場合はそれぞれの環境ファイルに記述を追加してください。


## まとめ

### RubyとElixirの比較表

|Ruby|Elixir|役割|
|---|----|---|
|bundler|mix|プロジェクト管理ツール、ライブラリ依存ツール|
|Gemfile|mix.exs|プロジェクトで必要なライブラリを記述|
|bundle install|mix deps.get|ライブラリ ダウンロード|
|Rubygems|Hex|ライブラリを管理する中央サーバー|
|Rails|Phoenix|フルスタック WEBフレームワーク|
|rerun|Phoenixに標準搭載|プログラムの更新を自動的に反映|
|Active Record|Ecto|O/Rマッパー|
|p|IO.inspect|変数のダンプ出力|

今回はMySQLを使いましたが、Elixir(BEAM VM)のパフォーマンス特性を活かすにはDBがボトルネックになるのでmemcachedを入れたりインメモリDBに置き換えるなどの対策がそのうち必要でしょう。

RailsやRailsライクフレームワークを触ったことがある人は、Elixirという言語の文法をある程度理解できれば、難なくPhoenixでサーバーが構築できます。
コーディングの難易度もErlangに比べたら高くなく、スムーズにRailsから以降できるのでリクエストが多すぎて捌き切れない事が想定されるAPIサーバーは、Elixir Phoenixで作成することも検討に入れてもいいと思います。