「BFFを40秒で支度しな」を叶える(Phoenix軽量APIで)：複数APIを呼び出して自前JOIN （Inner JOIN→Outer JOINのオマケ付き）

[fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com/)のpiacereです
ご覧いただいて、ありがとうございます :bow:

[前作は、Phoenix軽量APIを使ったBFF的API構築](https://qiita.com/piacerex/items/2e4e6629222700bf3756)を、40秒という短時間で構築しましたが、今度は、複数APIを呼び出し、自前でJOINする、本物のBFFを作ります

今回も、<b>「40秒で支度しな」</b>、で行きます:yum:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/e6ed6a62-0e2c-e260-e8dd-b75371205dca.png)

なお「BFF」は、「Backend for Frontend」の略で、フロントエンド毎でプレーンAPI呼出を前提としてロジックを書くのを止め、APIファサードでビジネスロジックを共通提供する考え方で、これにより、フロントエンド毎にビジネスロジックを開発するムダを省きます（詳しくは下記）

[<b>BFF（Backends For Frontends）超入門 ― Netflix、Twitter、リクルートテクノロジーズが採用する理由</b>](https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html)
https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.10.1 　　　　 ※[<b>最新版のインストール手順はコチラ</b>](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- Phoenix 1.4.15　　　※[<b>最新版のインストール手順はコチラ</b>](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- Node.js 12.14.0

本内容の実施前提として、下記コラムの「事前準備Ａ～Ｅ」は終えておいてください

[<b>「BFF的APIを40秒で支度しな」：Phoenix軽量APIでScaffoldした2テーブルをJOIN</b>](https://qiita.com/piacerex/items/2e4e6629222700bf3756)
https://qiita.com/piacerex/items/2e4e6629222700bf3756

更に、本コラム中盤以降の「事前準備Ｆ」も終えておいてください

# 手順①【0秒～36秒】：JSONテンプレートを書く

ここから、40秒間の本番です

投入済みのDBデータを、API2本の呼び出しで取得し、team_idの一致でJOINに相当するデータ処理を行うだけです

```elixir:lib/bff_web/templates/api/v1/bff/team_members/index.json.eex
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返却されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/dd70ef21-28b1-9d75-37a9-8dd0f68f444a.png)

# 「今回も、これだけで完成です」

はい、今回もたったこれだけ、40秒でBFF構築が完了しました

前回同様、JSONテンプレートをいじるだけで、カスタマイズやメンテナンスが可能ですし、APIバージョンアップやエントリーポイント変更も、フォルダ移動するだけだし、別のBFFを生やしたいときも、フォルダ掘って、JSONテンプレート置くだけです（ルーティング／MVCは不要）

ちなみに、シリーズ中の下記残対応についても、今回、検証できました

## <b>■全体バランス取りなど</b>

- Mnesiaの独自実装では無く、Ecto Queryでロジックを組んだときの配置は大丈夫か？

# ここまでの連載で感じて欲しいこと

軽量APIは、JSONテンプレートを追加するだけでAPI／BFF構築できる点が特徴ですが、JSONテンプレートの中身を、今一度、見てください

Elixirプログラミングに慣れている方であれば、このJSONテンプレートの記述が、

<font color="red"><b>「実に、Elixirらしいデータ処理開発に、集中できている」</b></font>

という点に気付かれたのでは無いかと思います

そう、このPhoenix軽量APIは、PHPのような気軽さを提供しつつ、余計な雑事に煩わされること無く、

<font color="red"><b>よりElixirらしいコードに集中したい</b></font>

という、「Elixir大好きオタク」的エッセンスが、パッケージングされているのです:stuck_out_tongue_winking_eye:

# 「ところで、team_idがマッチしない場合はどうなる？」

前回は、Ecto.Queryの機能で、Inner JOINがSQL同様、暗黙でかかり、マッチしたもの同士だけが取得できましたが、今回のBFFでは、どうでしょう？

試しに、``「PUT http://localhost:4000/users/4」``で、存在しないteam_idに書き換えてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9353f363-2687-6fdc-7dfb-30d78bbe676f.png)

``「GET http://localhost:4000/api/v1/bff/team_members」``で確認すると、Outer JOINしていることが分かります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/fd4d36b0-00c0-ac38-fef0-5771fef47772.png)

ここをInner Joinに変更すると、下記のようになります

リストでnilだったものを最後にEnum.filter()で除外するところがポイントです

```elixir:lib/bff_web/templates/api/v1/bff/team_members/index.json.eex
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になりました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5a35b854-4941-3fa4-5b87-aa49240e1850.png)

# 付録【事前準備Ｆ】：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」のコメントアウトも行ってください

```lib/bff_web/router.ex
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」``で追加しましょう

```json
{
    "user": 
    {
        "name": "hisaway",
        "age": 23,
        "team_id": 2
    } 
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ba2b751a-5e27-ec7f-2dd3-fd06178ea75e.png)

``「GET http://localhost:4000/users」``でデータが追加されたことが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/77daa62f-8815-220b-6175-4a91248c35c0.png)

``「GET http://localhost:4000/teams」``の一覧も問題無いことも確認します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/20f51705-8d57-746c-0925-7977c1af94d1.png)

# 終わり

Phoenix軽量APIを使い、JSONテンプレート追加だけで、BFFを構築できました

また、Outer JOINからInner JOINに切り替える、JSONテンプレートのカスタマイズも行いました

40秒くらいで、本当に構築できるということが実感いただけたら幸いです

そして、この軽量APIの裏に流れる、

<font color="red"><b>よりElixirらしいコードに集中したい</b></font>

という、「Elixir大好きオタク」ならではのマインドを感じていただけたら嬉しいです

[次回は、軽量APIのREST API対応に戻り、下記の残対応に着手](https://qiita.com/piacerex/items/9e507ae2f75c1e2edefd)します

## <b>■エラー処理　※JSON内でラクに書く？</b>

- show.json.eex／update.json.eex／delete.json.eexのデータ未存在時の404エラー
- サーバエラー時の500エラー

## <b>■コード最適化余地</b>

- JSONテンプレート評価の共通関数化（ApiView.render()と、ApiController.execute()が同じ処理）
- :mnesia.start～:mnesia.wait_for_tables()の共通関数化
- ApiControllerの各関数で、パス処理が同じなので、その共通関数化
- DbMnesiaの返却の共通関数化
- index.json.eex／show.json.eexやApiController.execute()のファイルパス指定を無くしたい

## <b>■全体バランス取りなど</b>

- どこまでをAPIコントローラ側に置き、どこまでをJSONテンプレートに置くか？
- Mnesiaの独自実装では無く、Ecto Queryでロジックを組んだときの配置は大丈夫か？
- APIでは無く、WebアプリやLiveViewでも、JSONテンプレートを応用できないか？

## <b>■Mnesiaアクセッサの不足機能補充　※どこまでマジメにやる？</b>

- index.json.eex／show.json.eexのselect列指定対応
- insert.json.eexのSQLでのinsert列指定対応
- update.json.eexのSQLでのupdate対象列順変動／全列指定無し対応
- whereの「=」以外対応
- id以外のwhere対応（Mnesiaでそもそも実現できるかしら？）

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow: