Phoenixお気軽API開発⑦：更新REST APIの実装で軽量REST API完成(TDD付)

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

[前作で追加・削除REST APIに対応](https://qiita.com/piacerex/items/2c95ff8f01d4a9d2d241)し、今回の更新REST APIで、以下4つのHTTPメソッドが出揃い、いよいよ「軽量APIによるREST API」が完成です

- [<b>GET：一覧、1件参照</b>](https://qiita.com/piacerex/items/437e7b6c26c95dae0efb)
- [<b>POST：追加</b>](https://qiita.com/piacerex/items/94d9275e32ecd0e83da1)
- [<b>PUT：更新</b>](https://qiita.com/piacerex/items/78d3036e556e66981c79)
- [<b>DELETE：削除</b>](https://qiita.com/piacerex/items/94d9275e32ecd0e83da1)

# 本コラムの検証環境

本コラムは、以下環境で検証しています（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

あと下記コラムシリーズの続きとして実施するので、未実施であれば、事前に実施しておいてください（近々、ボイラープレート生成のmixコマンドとしてOSS化は考えています）

[<b>PHP的ハックを応用してNode.js Express／Go的な軽量APIをPhoenixで実現してみた</b>](https://qiita.com/piacerex/items/144089aa6f2052f4d1cb)
|> [<b>Express／Go的なPhoenix軽量APIの上に参照系REST APIを実装する</b>](https://qiita.com/piacerex/items/e11ef7b5cdcc81ac69c9)
|> [<b>軽量APIにて実装した一覧REST APIにDBを接続する</b>](https://qiita.com/piacerex/items/437e7b6c26c95dae0efb)
|> [<b>DB取得＋軽量APIで1件参照REST APIを実装する(TDD付)</b>](https://qiita.com/piacerex/items/7c969e42e7cf4fa5ed50)
|> [<b>DB追加／削除＋軽量APIで更新系REST API（暫定版）を実装する(TDD付)</b>](https://qiita.com/piacerex/items/94d9275e32ecd0e83da1)
|> [<b>軽量APIの追加・削除REST APIを完成させる</b>](https://qiita.com/piacerex/items/2c95ff8f01d4a9d2d241)

# 手順①：DBアクセッサにupdate処理を追加

## １）DbMnesiaモジュールにupdate処理を追加

DbMnesiaモジュールに、update()を追加し、サブ関数はTDDします

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	def update( table_name, sets, wheres ) do
		:mnesia.start
		table_atom = table_name |> String.to_atom
		:mnesia.wait_for_tables( [ table_atom ], 1000 )
		where_value = wheres |> wheres_value |> elem( 0 )
		update_spec = sets |> sets_value |> Tuple.insert_at( 0, where_value ) |> Tuple.insert_at( 0, table_atom )
		result = :mnesia.transaction( fn -> :mnesia.write( update_spec ) end )
		case result do
			{ :atomic,  :ok } -> { :ok }
			{ :aborted, err } -> { :error, err }
			{ _,        err } -> { :error, err }
			err               -> { :error, err }
		end
	end
…
	@doc """
	SQL Sets strings(list) to value list

	## Examples
		iex> DbMnesia.sets_value( [ "id = 123", "name = 'hoge'", "team = 'foo'" ] )
		{ 123, "hoge", "foo" } 
	"""
	def sets_value( sets ) do
		{}
	end
…
```

## ２）空を返し、テスト失敗を確認（TDD Red）

上記ファイルを保存すると、テストが自動で走り、期待通り、テスト失敗します（TDD Redパターン）

```
......

  1) doctest DbMnesia.sets_value/1 (2) (DbMnesiaDocTest)
     test/doc_test.exs:8
     Doctest failed
     doctest:
       iex> DbMnesia.sets_value( [ "id = 123", "name = 'hoge'", "team = 'foo'" ] )
       { 123, "hoge", "foo" }
     code:  DbMnesia.sets_value(["id = 123", "name = 'hoge'", "team = 'foo'"]) === { 123, "hoge", "foo" }
     left:  {}
     right: {123, "hoge", "foo"}
     stacktrace:
       lib/util/db_mnesia.ex:131: DbMnesia (module)

...

Finished in 0.5 seconds
7 doctests, 3 tests, 1 failure
```

## ３）期待値をリテラルで返し、テスト成功を確認（TDD Green）

期待値そのままを固定値で返すよう、ロジック側を修正します

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	def sets_value( sets ) do
		{ 123, "hoge", "foo" } 
	end
…
```

保存すると、テストが自動的に走り、もちろんテスト成功します（TDD Greenパターン）

```
..........

Finished in 0.4 seconds
7 doctests, 3 tests, 0 failures
```

各関数に、2つ目のテストパターンを追加すると、ロジックは固定値しか返していないため、テスト失敗します（TDD Redパターン）

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	@doc """
	SQL Sets strings(list) to value list

	## Examples
		iex> DbMnesia.sets_value( [ "id = 123", "name = 'hoge'", "team = 'foo'" ] )
		{ 123, "hoge", "foo" } 
		iex> DbMnesia.sets_value( [ "name = 'hogehoge'", "position = 'bar'" ] )
		{ "hogehoge", "bar" } 
	"""
	def sets_value( sets ) do
…
```

## ４）ロジック実装し、テスト成功を確認（TDD Refactoring）

ここで、テストが通る最低限のロジックを実装します

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	def sets_value( sets ) do
		sets
		|> Enum.reduce( {}, fn set, acc ->
			kv = String.split( set, "=" ) |> Enum.map( & String.trim( &1 ) )
			v = List.last( kv )
			Tuple.append( acc, raw_value( v ) )
		end )
	end
…
```

保存すると、テストが自動的に走り、テスト成功が確認できます（TDD Refactoringパターン）

```
..........

Finished in 0.4 seconds
7 doctests, 3 tests, 0 failures
```

## ６）Dbモジュールにupdate処理を追加

Dbモジュールに、update()を追加します

```elixir:lib/util/db.ex
defmodule Db do
…
	def update( sql ) do
		parsed_map = Regex.named_captures( ~r/update( *)(?<tables>.*)( *)set( *)(?<sets>.*)( *)where( *)(?<wheres>.*)/, sql )
		DbMnesia.update( 
			parsed_map[ "tables" ]  |> string_to_list |> List.first, 
			parsed_map[ "sets"   ]  |> string_to_list, 
			parsed_map[ "wheres" ]  |> string_to_list )
	end
…
```

# 手順②：update.json.eex内でDBデータ更新

上記のupdate対応したDBアクセッサを使って、データ更新SQLを投げるJSONテンプレート、update.json.eexを作成します

```elixir:lib/basic_web/templates/api/v1/users/update.json.eex
data = params[ "data" ]
Db.query( "update members set name = '#{ data[ "name" ] }', age = #{ data[ "age" ] }, team = '#{ data[ "team" ] }', position = '#{ data[ "position" ] }' where id = #{ params[ "id" ] }" )
```

# 手順③：updateのAPIコントローラ実装不要に

APIコントローラに、PUTで受けたリクエストJSONでデータ更新するためのupdate()を追加します

create()同様、リクエストJSON内の"data"にデータが入っている前提とします

```elixir:lib/basic_web/controllers/api_controller.ex
defmodule BasicWeb.ApiController do
…
  def update( conn, params ) do
    id = params[ "path_" ] |> List.last |> Type.to_number
    new_params = params |> Map.put( "id", id )
    path = params[ "path_" ] |> Enum.drop( -1 )
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 

    # TODO：data.json.eexと列突合チェック
    result = execute( "#{ prefix }update.json", params: new_params )

    if elem( result, 0 ) == :ok do
      new_params = params |> Map.put( "id", elem( result, 1 ) )
      conn
      |> put_status( :created )
      |> render( "#{ prefix }show.json", params: new_params )
    else
      result
    end
  end
…
```

# 手順④：動作確認

まず、更新元となるデータを、RESTクライアント``「GET http://localhost:4000/api/v1/users/6」``で確認します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b1f8c965-ae02-eb73-4ed3-263afc5147b6.png)

下記のリクエストbodyを設定します

```json
{
    "data": 
    {
        "name": "テストユーザ6",
        "age": 36,
        "team": "カスタマーチーム",
        "position": "問い合わせ担当"
    } 
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/2a8a034a-47b2-cf3e-4c9f-e84ba5426e2f.png)

``「PUT http://localhost:4000/api/v1/users/6」``でデータ更新が行われます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/0e65801f-bbbe-1511-77ac-7378f79458ce.png)

``「GET http://localhost:4000/api/v1/users/6」``を再度行うと、更新されたデータが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/d1f293bd-619c-78ff-e97d-b537fbfcb51b.png)

``「GET http://localhost:4000/api/v1/users」``でも、更新されたデータが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/11f791a6-4dd2-3f12-6519-708b0d679eb5.png)

# 終わり

JSONテンプレートでDBデータ更新できるようになりました

これで、各種JSONテンプレートを配置すれば、REST APIの実装が完了するようになりました

最終的なコードは、以下にまとめてあります

[<b>Phoenix軽量APIを使ったREST APIのコード（データはMnesia DBに保持）</b>](https://qiita.com/piacerex/items/a41176fb0c7c68523a4b)
https://qiita.com/piacerex/items/a41176fb0c7c68523a4b

今回の内容が上手く理解できなかった方は、上記からコラムを追って、まず手元で動かすところから始めてみてください

なお今回で、REST APIの実装は一通り完成ですが、下記の対応もろもろが残っているので、この辺りを詰めていきますが、現時点で、それなりに軽量REST APIが使える状態なので、[次回は改めてREST APIをサクっと構築し、SPAでも作っていきたい](https://qiita.com/piacerex/items/2e4e6629222700bf3756)と思います

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

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

<b>■不足機能　※どこまでマジメにやるか？下記よりもjoinやorder by、group byとか欲しいかも？</b>

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

<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テンプレートを応用できないか？

# 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: