Phoenixお気軽API開発⑤：DB追加／削除＋軽量APIで更新系REST API（暫定版）を実装する(TDD付)

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

[前作までの参照系REST API実装](https://qiita.com/piacerex/items/7c969e42e7cf4fa5ed50)に続き、今回は、POST、つまり「新規REST API」を実装します

ただし今回は、DBアクセッサの実装がメインで、軽量APIらしいJSONテンプレートの連動は行わず、APIコントローラも暫定実装としています

なお、本コラムシリーズの永続系DBとして利用している「Mnesia」は、[Elixir専用PaaS「Gigalixir」](https://gigalixir.com/)においても、DB利用料がチャージされず、最低価格が月\$10から運用でき、リーズナブルです（PostgreSQLを利用すると最低価格でも\$35かかる）

# 本コラムの検証環境

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

# 手順①：JSONテンプレートにid追加

今回、追加や削除で、idの確認が必要になるため、JSONテンプレートにidを追加します

```elixir:lib/basic_web/templates/api/v1/users/data.json.eex
%{
  name:     data[ "name" ], 
  age:      data[ "age" ], 
  team:     data[ "team" ], 
  position: data[ "position" ]
}
```

RESTクライアントで``「GET http://localhost:4000/api/v1/users」``のアクセスを行うと、idが追加されることを確認してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5bcdeac8-65a8-4980-ba81-c58128f60ad6.png)

# 手順②：DBアクセッサにinsert／delete処理を追加

## １）DbMnesiaモジュールにinsert／delete処理を追加

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

なおinsertは、列選択を省略したSQLを想定しています

deleteは、idのみ指定可能なSQLを想定します

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
	def insert( table_name, _columns, values ) do
		:mnesia.start
		table_atom = table_name |> String.to_atom
		:mnesia.wait_for_tables( [ table_atom ], 1000 )
		# TODO: 列選択はそのうち
		# TODO: 自動発番は、以降で実装
		insert_spec = values |> values_value |> Tuple.insert_at( 0, table_atom )
		:mnesia.transaction( fn -> :mnesia.write( insert_spec ) end )
	end

	def delete( table_name, wheres ) do
		:mnesia.start
		table_atom = table_name |> String.to_atom
		:mnesia.wait_for_tables( [ table_atom ], 1000 )
		delete_spec = wheres |> wheres_value |> Tuple.insert_at( 0, table_atom )
		:mnesia.transaction( fn -> :mnesia.delete( delete_spec ) end )
	end

	@doc """
	SQL Values strings(list) to value list

	## Examples
		iex> DbMnesia.values_value( [ "123", "'hoge'" ] )
		{ 123, "hoge" } 
	"""
	def values_value( values ) do
		{}
	end

	@doc """
	SQL Where strings(list) to value list

	## Examples
		iex> DbMnesia.wheres_value( [ "id = 123" ] )
		{ 123 } 
	"""
	def wheres_value( wheres ) do
		{}
	end
…
```

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

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

```
...

1) doctest DbMnesia.wheres_value/1 (3) (DbMnesiaDocTest)
     test/doc_test.exs:8
     Doctest failed
     doctest:
       iex> DbMnesia.wheres_value( [ "id = 123" ] )
       { 123 }
     code:  DbMnesia.wheres_value(["id = 123"]) === { 123 }
     left:  {}
     right: {123}
     stacktrace:
       lib/util/db_mnesia.ex:85: DbMnesia (module)

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

..

Finished in 0.4 seconds
5 doctests, 3 tests, 2 failure
```

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

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

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	def values_value( values ) do
		{ 123, "hoge" }
	end
…
	def wheres_value( wheres ) do
		{ 123 }
	end
…
```

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

```
......

Finished in 0.3 seconds
5 doctests, 3 tests, 0 failures
```

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

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

	## Examples
		iex> DbMnesia.values_value( [ "123", "'hoge'" ] )
		{ 123, "hoge" } 
		iex> DbMnesia.values_value( [ "987", "'foo'" ] )
		{ 987, "foo" } 
	"""
	def values_value( values ) do
…
	@doc """
	SQL Where strings(list) to value list

	## Examples
		iex> DbMnesia.wheres_value( [ "id = 123" ] )
		{ 123 } 
		iex> DbMnesia.wheres_value( [ "id = 987" ] )
		{ 987 } 
	"""
	def wheres_value( wheres ) do
…
```

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

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

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	def values_value( values ) do
		values 
		|> Enum.reduce( {}, fn v, acc ->
			raw_v = case Type.is( v ) do
				"Integer" -> v |> String.to_integer
				"Float"   -> v |> String.to_float
				_         -> v |> String.trim( "'" ) |> String.trim( "\"" )
			end
			Tuple.append( acc, raw_v )
		end )
	end
…
	def wheres_value( wheres ) do
		wheres
		|> Enum.reduce( {}, fn where, acc ->
			kv = String.split( where, "=" ) |> Enum.map( & String.trim( &1 ) )
			v = List.last( kv )
			raw_v = case Type.is( v ) do
				"Integer" -> v |> String.to_integer
				"Float"   -> v |> String.to_float
				_         -> v |> String.trim( "'" ) |> String.trim( "\"" )
			end
			Tuple.append( acc, raw_v )
		end )
	end
A…
```

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

```
...

Finished in 0.3 seconds
5 doctests, 3 tests, 0 failures
```

## ５）共通処理を関数化する

ここで、wheres_param()／values_value()／wheres_value()内の、raw_vを算出する処理が、全く同じなので、raw_value()として関数化します

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	@doc """
	SQL Where strings(list) to Mnesia conditons(tuple list), but different only for columns name

	## Examples
		iex> DbMnesia.wheres_param( [ "id = 123", "name = 'hoge'", "team = 'foo'" ] )
		[
			{ :==, "id",   123    }, 
			{ :==, "name", "hoge" }, 
			{ :==, "team", "foo"  } 
		]
		iex> DbMnesia.wheres_param( [ "name = 'hogehoge'", "position = 'bar'" ] )
		[
			{ :==, "name",     "hogehoge" }, 
			{ :==, "position", "bar"  } 
		]
	"""
	def wheres_param( wheres ) do
		wheres
		|> Enum.map( fn where ->
			kv = String.split( where, "=" ) |> Enum.map( & String.trim( &1 ) )
			v = List.last( kv )
			{ :==, List.first( kv ), raw_value( v ) }
		end )
	end

	@doc """
	SQL Values strings(list) to value list

	## Examples
		iex> DbMnesia.values_value( [ "123", "'hoge'" ] )
		{ 123, "hoge" } 
		iex> DbMnesia.values_value( [ "987", "'foo'" ] )
		{ 987, "foo" } 
	"""
	def values_value( values ) do
		values 
		|> Enum.reduce( {}, fn v, acc ->
			Tuple.append( acc, raw_value( v ) )
		end )
	end

	@doc """
	SQL Where strings(list) to value list

	## Examples
		iex> DbMnesia.wheres_value( [ "id = 123" ] )
		{ 123 } 
		iex> DbMnesia.wheres_value( [ "id = 987" ] )
		{ 987 } 
	"""
	def wheres_value( wheres ) do
		wheres
		|> Enum.reduce( {}, fn where, acc ->
			kv = String.split( where, "=" ) |> Enum.map( & String.trim( &1 ) )
			v = List.last( kv )
			Tuple.append( acc, raw_value( v ) )
		end )
	end

	def raw_value( v ) do
		case Type.is( v ) do
			"Integer" -> v |> String.to_integer
			"Float"   -> v |> String.to_float
			_         -> v |> String.trim( "'" ) |> String.trim( "\"" )
		end
	end
…
```

保存すると、テストが自動的に走り、テスト成功が確認できます

こうしたリファクタリングが安全に行えることが、TDDの大きな恩恵です

```
...

Finished in 0.3 seconds
5 doctests, 3 tests, 0 failures
```

更に、raw_value()にもdoctestを追加しておきましょう

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
…
	@doc """
	String value to raw value

	## Examples
		iex> DbMnesia.raw_value( "" )
		""
		iex> DbMnesia.raw_value( "123" )
		123
		iex> DbMnesia.raw_value( "12.34" )
		12.34
		iex> DbMnesia.raw_value( "'hoge'" )
		"hoge"
		iex> DbMnesia.raw_value( "foo" )
		"foo"
		iex> DbMnesia.raw_value( "12ab3" )
		"12ab3"
	"""
	def raw_value( v ) do
…
```

## ６）Dbモジュールにinsert／delete処理を追加

Dbモジュールは、元々query()内にあったDB取得処理を、select()に移し、queryは、CRUDを呼び分ける処理に差し替えます

その上で、insert()とdelete()を追加します

なお、insert時のidは、自動発番にするべきところですが、次回以降に預け、今回は、リクエスト時に指定することにします

insert時の列選択も、次回以降に預けます

update()は、今回、空実装にし、以降で実装します

```elixir:lib/util/db.ex
defmodule Db do
	def query( sql ) when sql != "" do
		cond do
			String.match?( sql, ~r/select.*/ ) -> select( sql )
			String.match?( sql, ~r/insert.*/ ) -> insert( sql )
			String.match?( sql, ~r/update.*/ ) -> update( sql )
			String.match?( sql, ~r/delete.*/ ) -> delete( sql )
		end
	end

	def select( sql ) do
		if String.match?( sql, ~r/.*where.*/ ) do
			parsed_map = Regex.named_captures( ~r/select( *)(?<columns>.*)( *)from( *)(?<tables>.*)( *)where( *)(?<wheres>.*)/, sql )
			DbMnesia.select( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				parsed_map[ "columns" ] |> string_to_list, 
				parsed_map[ "wheres" ]  |> string_to_list )
		else
			parsed_map = Regex.named_captures( ~r/select( *)(?<columns>.*)( *)from( *)(?<tables>.*)/, sql )
			DbMnesia.select( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				parsed_map[ "columns" ] |> string_to_list, 
				[] )
		end
	end

	def insert( sql ) do
		if false do  # TODO: 列選択はそのうち
			parsed_map = Regex.named_captures( ~r/insert into( *)(?<tables>.*)\(( *)(?<columns>.*)( *)\)( *)values\(( *)(?<values>.*)( *)\)/, sql )
			DbMnesia.insert( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				parsed_map[ "columns" ] |> string_to_list, 
				parsed_map[ "values" ]  |> string_to_list )
		else
			parsed_map = Regex.named_captures( ~r/insert into( *)(?<tables>.*)( *)values\(( *)(?<values>.*)( *)\)/, sql )
			DbMnesia.insert( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				[], 
				parsed_map[ "values" ]  |> string_to_list )
		end
	end

	def update( _sql ) do
		# TODO: 以降で実装
	end

	def delete( sql ) do
		if String.match?( sql, ~r/.*where.*/ ) do
			parsed_map = Regex.named_captures( ~r/delete( *)from( *)(?<tables>.*)( *)where( *)(?<wheres>.*)/, sql )
			DbMnesia.delete( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				parsed_map[ "wheres" ]  |> string_to_list )
		else
			parsed_map = Regex.named_captures( ~r/delete( *)from( *)(?<tables>.*)/, sql )
			DbMnesia.delete( 
				parsed_map[ "tables" ]  |> string_to_list |> List.first, 
				[] )
		end
	end
…
```

# 手順②：create／deleteのAPIコントローラを暫定実装

APIコントローラに、POSTで受けたリクエストJSONでデータ追加するためのcreate()と、DELETEでデータ削除するためのdelete()を追加します

put_status()で、create()はステータスコード201（Created）を返却、delete()はステータスコード204（No Contents）を返却するようにします

update()は空振りで、以降で実装します

いずれも、リクエストJSON内の"data"にデータが入っている前提とします

なお、本来であれば、どのURLに対するPOST／DELETEかで、JSONテンプレートのパスを特定し、そこに置いてあるdata.json.eexの列と突き合わせすることで、妥当なリクエストかを判定するべきですが、ここの実装も以降に預け、今回は、コントローラ内から、直接DBアクセッサを呼び出します

それと、POST時のidは、insertでの自動発番の実装が次回以降となるので、今回は暫定的に、リクエストJSON内で指定することとします

```elixir:lib/basic_web/controllers/api_controller.ex
defmodule BasicWeb.ApiController do
…
  def create( conn, params ) do
    path = params[ "path_" ]
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 

    # TODO：URLからJSONテンプレートパスを特定／実行 ＆ data.json.eexと列突合チェック
    Db.query( "insert into members values( #{ params[ "id" ] }, '#{ data[ "name" ] }', '#{ data[ "age" ] }', '#{ data[ "team" ] }', '#{ data[ "position" ] }' )" )

    conn
    |> put_status( :created )
    |> render( "#{ prefix }show.json", params: params )
  end

  def update( _conn, _params ) do
    # TODO: 以降で実装
  end

  def delete( 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：URLからJSONテンプレートパスを特定／実行 ＆ data.json.eexと列突合チェック
    Db.query( "delete from members where id = #{ new_params[ "id" ] }" )

    send_resp( conn, :no_content, "" )
  end
end
```

# 手順③：動作確認

## １）追加REST APIの動作確認

RESTクライアントで、下記のリクエストbodyを設定します

```json
{
	"id": 123,
	"data": 
	{
		"name": "テストユーザ123", 
		"age": 45, 
		"team": "テストチーム", 
		"position": "テスト担当"
	}
}
```

RESTクライアント画面では、下記の通り、URL直下の「Body」を押下し、「raw」ラジオボタンを押下すると、リクエストbodyが入力できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1d28cb90-5847-f0a4-d438-4e71e45e00fb.png)

それから、``「POST http://localhost:4000/api/v1/users」``でデータ追加が行われます（ステータスコードが201 Created）になっています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5d10f92e-68ea-5afb-3e19-0084105b5b1c.png)


``「GET http://localhost:4000/api/v1/users」``で確認すると、追加されたデータが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/89712d27-f4ec-5953-80f0-49b33b43bf9d.png)

``「GET http://localhost:4000/api/v1/users/123」``で1件表示でも確認してみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a5f84314-f29c-69ed-6742-f4bc34801b06.png)

## ２）削除REST APIの動作確認

今度は、``「DELETE http://localhost:4000/api/v1/users/123」``でデータ削除してみます（ステータスコードが201 Created）になっています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/31de5252-5f02-2aef-ca16-886cddeab601.png)

``「GET http://localhost:4000/api/v1/users」``で確認すると、データが削除されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/beffd62c-c104-39de-6290-acea91a091a2.png)

# 終わり

軽量APIらしいJSONテンプレートの連動は行わず、APIコントローラも暫定実装ではありますが、追加・削除REST APIが動くようになりました

参照系・追加・削除が揃ったので、だいぶREST APIらしさが出てきたと思います

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

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

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

[次回は、追加・削除REST APIを、暫定版から本格版に作り直します](https://qiita.com/piacerex/items/2c95ff8f01d4a9d2d241)

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