fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます
Phoenixのmix phx.gen.jsonで作成したAPIは、DB(テーブル)と1対1のCRUD APIを自動生成してくれて便利なので、mix phx.gen.jsonした結果を改造して、DB CURD以外のJSONを返すAPIを作る手順をまとめてみます
なお、mix phx.gen.json自体の使い方については、Elixir入門「第3回:Phoenix 1.3で高速webアプリ & REST APIアプリをサクッと書いてみる」のP22以降をご覧ください
mix phx.gen.jsonが生成するcontroller/viewの構造
上記スライドに挙げている例の通り、mix phx.gen.jsonでAPIを作成します
mix phx.gen.json Tools Post posts title:string body:text
すると、controllersフォルダ配下に、以下のようなモジュールが作成されます
defmodule ApiWeb.PostController do
…
def index(conn, _params) do
api = Tools.list_posts()
render(conn, "index.json", api: api)
end
この自動生成されたコントローラは、index()であれば、テーブルデータ全件のマップリストを、render()に渡し、JSONデータとして返却します
なお、index()以外も、だいたい同じ構造となっています
テーブルデータ全件を取得するRepo.all()は、以下のような定義が自動生成されます(index()以外が使うCRUD関数も、このモジュール中に定義されています)
defmodule Api.Tools do
…
def list_posts do
Repo.all(Post)
end
…
その後、呼ばれるrender()は、ビューとして自動生成され、3つの関数で構成されます
defmodule ApiWeb.PostView do
use SampleAnalyticsWeb, :view
alias SampleAnalyticsWeb.PostView
def render("index.json", %{posts: posts}) do
%{data: render_many(posts, PostView, "post.json")}
end
def render("show.json", %{post: post}) do
%{data: render_one(post, PostView, "post.json")}
end
def render("post.json", %{post: post}) do
%{id: post.id,
title: post.title,
body: post.body}
end
index()が使うのは、1つ目の関数と、3つ目の関数です
1つ目の関数(index)で使われるrender_many()は、複数件のマップリストとして返却します
2つ目の関数(show)で使われるrender_one()は、単品のマップとして返却します
3つ目の関数は、render_many()とrender_one()のどちらからも呼ばれる、単品マップを返却するためのコールバック的な関数で、ここでDB列を表す構造体を、返却するJSONに相当するマップへと変換しています(render_many()は、この関数を、各行毎に呼び出しています)
DB CURD以外のJSONを返却するAPIを作る
DB CURD以外のJSONを返却するAPIを作るためには、コントローラにDB CRUD以外の任意の処理をさせ、ビューのrender()では、DB列の構造体で無いものをマップとして返せば良い、ということです
まず、コントローラからrender()に渡すデータを変更します
下記の例では、元々あったテーブルデータ全件を返却する代わりに、Excelionを使って、Excelデータを読み込み、そのマップリストを返却しています(なお、Excelは、1行目に列ヘッダーがあるデータを想定しており、1行目は読み飛ばしています)
defmodule ApiWeb.PostController do
…
def index(conn, _params) do
api = "sample.xlsx"
|> Excelion.parse!( 0, 1 )
|> Enum.drop( 1 )
|> MapList.zip_atom( &( Enum.zip( [ columns, &1 ] ) ) )
|> Enum.map( &( &1 |> Enum.reduce( %{}, fn { key, value }, acc -> Map.put( acc, key |> String.to_atom, value ) end ) ) )
render(conn, "index.json", api: api)
end
ビューのrender()の方は、controllerから渡されたマップリストをそのまま返却すればOKなので、DB列の構造体に置き換えしていた部分を、引数そのままで返却するように変えます
defmodule ApiWeb.PostView do
…
def render("post.json", %{post: post}) do
post
end
end
こんな感じで、DB CURD以外のJSONを返すAPIが作れます
マップリスト/マップを返せばJSON APIが作られる世界へ
上記のやったことをまとめると、以下の通りです
① コントローラで、任意のマップリスト/マップを作る
② ビューのrender()は引数をそのまま返す
ここには、他言語だと当たり前な、JSONをチマチマ組み立てる処理は、一切、出てきません
にも関わらず、JSON APIがちゃんと定義できてしまう…という、ちょっとした魔法のような出来事です
Elixirの強力なリスト処理をうまく応用した、Phoenixの造りに脱帽です
なお、APIのURLを変えたり、モジュール名を変えるコツ、ビューのrender()を使わずに処理する方法などもありますが、これはまた別のコラムにて