fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
前作は、Phoenix軽量APIを使ったBFF的API構築を、40秒という短時間で構築しましたが、今度は、複数APIを呼び出し、自前でJOINする、本物のBFFを作ります
なお「BFF」は、「Backend for Frontend」の略で、フロントエンド毎でプレーンAPI呼出を前提としてロジックを書くのを止め、APIファサードでビジネスロジックを共通提供する考え方で、これにより、フロントエンド毎にビジネスロジックを開発するムダを省きます(詳しくは下記)
BFF(Backends For Frontends)超入門 ― Netflix、Twitter、リクルートテクノロジーズが採用する理由
https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html
本コラムの検証環境
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.10.1 ※最新版のインストール手順はコチラ
- Phoenix 1.4.15 ※最新版のインストール手順はコチラ
- Node.js 12.14.0
本内容の実施前提として、下記コラムの「事前準備A~E」は終えておいてください
「BFF的APIを40秒で支度しな」:Phoenix軽量APIでScaffoldした2テーブルをJOIN
https://qiita.com/piacerex/items/2e4e6629222700bf3756
更に、本コラム中盤以降の「事前準備F」も終えておいてください
手順①【0秒~36秒】:JSONテンプレートを書く
ここから、40秒間の本番です
投入済みのDBデータを、API2本の呼び出しで取得し、team_idの一致でJOINに相当するデータ処理を行うだけです
users = Json.get( "http://localhost:4000", "/users" )
teams = Json.get( "http://localhost:4000", "/teams" )
users[ "data" ]
|> Enum.map( fn
%{
"name" => name,
"age" => age,
"team_id" => team_id
}
->
team = teams[ "data" ]
|> Enum.filter( & &1[ "id" ] == team_id )
|> List.first
%{
"name" => name,
"age" => age,
"team" => team[ "name" ],
"url" => team[ "url" ]
}
end )
手順②【36秒~40秒】:動作確認
RESTクライアントで「GET http://localhost:4000/api/v1/bff/team_members」
のアクセスを行うと、以下のように、JOIN済みデータ一覧が、JSON返却されます
「今回も、これだけで完成です」
はい、今回もたったこれだけ、40秒でBFF構築が完了しました
前回同様、JSONテンプレートをいじるだけで、カスタマイズやメンテナンスが可能ですし、APIバージョンアップやエントリーポイント変更も、フォルダ移動するだけだし、別のBFFを生やしたいときも、フォルダ掘って、JSONテンプレート置くだけです(ルーティング/MVCは不要)
ちなみに、シリーズ中の下記残対応についても、今回、検証できました
■全体バランス取りなど
- Mnesiaの独自実装では無く、Ecto Queryでロジックを組んだときの配置は大丈夫か?
ここまでの連載で感じて欲しいこと
軽量APIは、JSONテンプレートを追加するだけでAPI/BFF構築できる点が特徴ですが、JSONテンプレートの中身を、今一度、見てください
Elixirプログラミングに慣れている方であれば、このJSONテンプレートの記述が、
「実に、Elixirらしいデータ処理開発に、集中できている」
という点に気付かれたのでは無いかと思います
そう、このPhoenix軽量APIは、PHPのような気軽さを提供しつつ、余計な雑事に煩わされること無く、
よりElixirらしいコードに集中したい
という、「Elixir大好きオタク」的エッセンスが、パッケージングされているのです
「ところで、team_idがマッチしない場合はどうなる?」
前回は、Ecto.Queryの機能で、Inner JOINがSQL同様、暗黙でかかり、マッチしたもの同士だけが取得できましたが、今回のBFFでは、どうでしょう?
試しに、「PUT http://localhost:4000/users/4」
で、存在しないteam_idに書き換えてみましょう
「GET http://localhost:4000/api/v1/bff/team_members」
で確認すると、Outer JOINしていることが分かります
ここをInner Joinに変更すると、下記のようになります
リストでnilだったものを最後にEnum.filter()で除外するところがポイントです
users = Json.get( "http://localhost:4000", "/users" )
teams = Json.get( "http://localhost:4000", "/teams" )
users[ "data" ]
|> Enum.map( fn
%{
"name" => name,
"age" => age,
"team_id" => team_id
}
->
team = teams[ "data" ]
|> Enum.filter( & &1[ "id" ] == team_id )
|> List.first
if team != nil do
%{
"name" => name,
"age" => age,
"team" => team[ "name" ],
"url" => team[ "url" ]
}
end
end )
|> Enum.filter( & &1 != nil )
「GET http://localhost:4000/api/v1/bff/team_members」
で確認すると、Inner Joinになりました
付録【事前準備F】:2テーブル分のAPI CRUDをScaffold
Phoenix標準機能のScaffold(mix phx.gen.html)を使って、2テーブル分のAPI CRUDを自動生成します
なお、テーブルおよびテストデータは、前回投入したものを、そのまま流用するので、前回のWeb CRUDを上書きするようにScaffoldします
Phoenixを一度落とし、下記コマンドでScaffoldします
mix phx.gen.json Api User users name:string age:integer team_id:integer
Would you like to proceed? [Yn] Y
Proceed with interactive overwrite? [Yn] Y
lib/bff_web/controllers/user_controller.ex already exists, overwrite? [Yn] Y
lib/bff_web/views/user_view.ex already exists, overwrite? [Yn] Y
test/bff_web/controllers/user_controller_test.exs already exists, overwrite? [Yn] Y
mix phx.gen.json Api Team teams name:string url:string
Would you like to proceed? [Yn] Y
Proceed with interactive overwrite? [Yn] Y
lib/bff_web/controllers/team_controller.ex already exists, overwrite? [Yn] Y
lib/bff_web/views/team_view.ex already exists, overwrite? [Yn] Y
test/bff_web/controllers/team_controller_test.exs already exists, overwrite? [Yn] Y
前回のWeb CRUDを、API CRUD2用のルーティングで上書きします
なお、CSRF対策を解除する必要もあるので、「:protect_from_forgery」のコメントアウトも行ってください
defmodule BffWeb.Router do
…
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
# plug :protect_from_forgery # <-- remove here
plug :put_secure_browser_headers
end
…
scope "/", BffWeb do
…
resources "/users", UserController, except: [:new, :edit] # <-- replace here
resources "/teams", TeamController, except: [:new, :edit] # <-- replace here
end
…
Phoenix起動します(マイグレーションは不要です)
iex -S mix phx.server
一応、API単品の動作確認をしておきます
RESTクライアントで、下記データを、「POST http://localhost:4000/users」
で追加しましょう
{
"user":
{
"name": "hisaway",
"age": 23,
"team_id": 2
}
}
「GET http://localhost:4000/users」
でデータが追加されたことが確認できます
「GET http://localhost:4000/teams」
の一覧も問題無いことも確認します
終わり
Phoenix軽量APIを使い、JSONテンプレート追加だけで、BFFを構築できました
また、Outer JOINからInner JOINに切り替える、JSONテンプレートのカスタマイズも行いました
40秒くらいで、本当に構築できるということが実感いただけたら幸いです
そして、この軽量APIの裏に流れる、
よりElixirらしいコードに集中したい
という、「Elixir大好きオタク」ならではのマインドを感じていただけたら嬉しいです
次回は、軽量APIのREST API対応に戻り、下記の残対応に着手します
■エラー処理 ※JSON内でラクに書く?
- show.json.eex/update.json.eex/delete.json.eexのデータ未存在時の404エラー
- サーバエラー時の500エラー
■コード最適化余地
- JSONテンプレート評価の共通関数化(ApiView.render()と、ApiController.execute()が同じ処理)
- :mnesia.start~:mnesia.wait_for_tables()の共通関数化
- ApiControllerの各関数で、パス処理が同じなので、その共通関数化
- DbMnesiaの返却の共通関数化
- index.json.eex/show.json.eexやApiController.execute()のファイルパス指定を無くしたい
■全体バランス取りなど
- どこまでをAPIコントローラ側に置き、どこまでをJSONテンプレートに置くか?
- Mnesiaの独自実装では無く、Ecto Queryでロジックを組んだときの配置は大丈夫か?
- APIでは無く、WebアプリやLiveViewでも、JSONテンプレートを応用できないか?
■Mnesiaアクセッサの不足機能補充 ※どこまでマジメにやる?
- index.json.eex/show.json.eexのselect列指定対応
- insert.json.eexのSQLでのinsert列指定対応
- update.json.eexのSQLでのupdate対象列順変動/全列指定無し対応
- whereの「=」以外対応
- id以外のwhere対応(Mnesiaでそもそも実現できるかしら?)