fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
前作で追加・削除REST APIに対応し、今回の更新REST APIで、以下4つのHTTPメソッドが出揃い、いよいよ「軽量APIによるREST API」が完成です
本コラムの検証環境
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.10.1 ※最新版のインストール手順はコチラ
- Phoenix 1.4.15 ※最新版のインストール手順はコチラ
- Node.js 12.14.0
あと下記コラムシリーズの続きとして実施するので、未実施であれば、事前に実施しておいてください(近々、ボイラープレート生成のmixコマンドとしてOSS化は考えています)
PHP的ハックを応用してNode.js Express/Go的な軽量APIをPhoenixで実現してみた
|> Express/Go的なPhoenix軽量APIの上に参照系REST APIを実装する
|> 軽量APIにて実装した一覧REST APIにDBを接続する
|> DB取得+軽量APIで1件参照REST APIを実装する(TDD付)
|> DB追加/削除+軽量APIで更新系REST API(暫定版)を実装する(TDD付)
|> 軽量APIの追加・削除REST APIを完成させる
手順①:DBアクセッサにupdate処理を追加
1)DbMnesiaモジュールにupdate処理を追加
DbMnesiaモジュールに、update()を追加し、サブ関数はTDDします
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
…
2)空を返し、テスト失敗を確認(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
3)期待値をリテラルで返し、テスト成功を確認(TDD Green)
期待値そのままを固定値で返すよう、ロジック側を修正します
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パターン)
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
…
4)ロジック実装し、テスト成功を確認(TDD Refactoring)
ここで、テストが通る最低限のロジックを実装します
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
6)Dbモジュールにupdate処理を追加
Dbモジュールに、update()を追加します
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を作成します
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"にデータが入っている前提とします
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」
で確認します
下記のリクエストbodyを設定します
{
"data":
{
"name": "テストユーザ6",
"age": 36,
"team": "カスタマーチーム",
"position": "問い合わせ担当"
}
}
「PUT http://localhost:4000/api/v1/users/6」
でデータ更新が行われます
「GET http://localhost:4000/api/v1/users/6」
を再度行うと、更新されたデータが表示されます
「GET http://localhost:4000/api/v1/users」
でも、更新されたデータが確認できます
終わり
JSONテンプレートでDBデータ更新できるようになりました
これで、各種JSONテンプレートを配置すれば、REST APIの実装が完了するようになりました
最終的なコードは、以下にまとめてあります
Phoenix軽量APIを使ったREST APIのコード(データはMnesia DBに保持)
https://qiita.com/piacerex/items/a41176fb0c7c68523a4b
今回の内容が上手く理解できなかった方は、上記からコラムを追って、まず手元で動かすところから始めてみてください
なお今回で、REST APIの実装は一通り完成ですが、下記の対応もろもろが残っているので、この辺りを詰めていきますが、現時点で、それなりに軽量REST APIが使える状態なので、次回は改めてREST APIをサクっと構築し、SPAでも作っていきたいと思います
■エラー処理 ※JSON内でラクに書く?
- show.json.eex/update.json.eex/delete.json.eexのデータ未存在時の404エラー
- サーバエラー時の500エラー
■不足機能 ※どこまでマジメにやるか?下記よりもjoinやorder by、group byとか欲しいかも?
- index.json.eex/show.json.eexのselect列指定対応
- insert.json.eexのSQLでのinsert列指定対応
- update.json.eexのSQLでのupdate対象列順変動/全列指定無し対応
- whereの「=」以外対応
- id以外のwhere対応(Mnesiaでそもそも実現できるかしら?)
■コード最適化余地
- 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テンプレートを応用できないか?