fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
前作までの追加・削除REST API実装は、暫定版で、以下が未完成でしたが、今回、①②を完成させます
①id自動発番(insert時)
②URLからJSONテンプレートパスの特定/実行(create時、delete時)
③data.json.eexと列突合チェック(create時、delete時)
ちなみに③は、更新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付)
残対応①:id自動発番
DbMnesiaのinsert()の中で、idのmaxを取得し、+1したものをinsertデータとして指定します
また、insert()成功時は、+1したidを返却します
defmodule DbMnesia do
def insert( table_name, _columns, values ) do
# TODO: 列選択はそのうち
:mnesia.start
table_atom = table_name |> String.to_atom
:mnesia.wait_for_tables( [ table_atom ], 1000 )
result = :mnesia.transaction( fn ->
next_id = max_id( table_atom ) + 1
insert_spec = values |> values_value |> Tuple.insert_at( 0, next_id ) |> Tuple.insert_at( 0, table_atom )
writed = :mnesia.write( insert_spec )
{ writed, next_id }
end )
case result do
{ :atomic, { :ok, id } } -> { :ok, id }
{ :aborted, { err, _ } } -> { :error, err }
{ _, { err, _ } } -> { :error, err }
{ err, _ } -> { :error, err }
end
end
def max_id( table_atom ) do
{ :atomic, keys } = :mnesia.transaction( fn -> :mnesia.all_keys( table_atom ) end )
case keys do
[] -> -1
_ -> keys |> Enum.max
end
end
…
APIコントローラからのinsert時、リクエストJSONのidを渡していたところを削除し、Db.insert()からの戻り値{ :ok, next_id }から、next_idをrender()時のidとして渡すようにします(エラー時はrender()せずエラー返却します)
なお、Dbモジュールは、DbMnesiaからの戻り値をそのまま素通しする造りなので、改修不要です
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と列突合チェック
result = Db.query( "insert into members values( '#{ data[ "name" ] }', '#{ data[ "age" ] }', '#{ data[ "team" ] }', '#{ data[ "position" ] }' )" ) # <-- modify here
# v-- add here
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
# ^-- add here
end
…
RESTクライアントで、idを削除した下記のリクエストbodyを設定します
{
"data":
{
"name": "テストユーザ124",
"age": 46,
"team": "開発チーム",
"position": "実装担当"
}
}
「POST http://localhost:4000/api/v1/users」
でデータ追加が行われ、idが、投入済みデータの最大である4の次の5が振られていることが確認できます
「POST http://localhost:4000/api/v1/users」
で再度データ追加すると、次は、id=6が振られます
残対応②:URLからJSONテンプレートパスの特定/実行
1)create/deleteのAPIコントローラを実装不要に
前回は、APIコントローラのcreate()とdelete()から、直接DBアクセッサを呼び出していた実装を、URLからJSONテンプレート(create.json.eex、delete.json.eex)を特定し、実行する実装に換装します(ついでに、delete()のエラー返却も追加しておくのと、それに合わせてDbMnesiaモジュール側も修正します)
defmodule BasicWeb.ApiController do
…
def create( conn, params ) do
path = params[ "path_" ]
prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/"
# TODO:data.json.eexと列突合チェック
result = execute( "#{ prefix }create.json", params: params ) # <-- replace here
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
…
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:data.json.eexと列突合チェック
result = execute( "#{ prefix }delete.json", params: new_params ) # <-- replace here
# v-- add here
if elem( result, 0 ) == :ok do
send_resp( conn, :no_content, "" )
else
result
end
# ^-- add here
end
def execute( path, params: params ) do
File.read!( "lib/basic_web/templates/api/#{ path }.eex" )
|> Code.eval_string( params: params, data: params[ "data" ] )
|> elem( 0 )
end
end
…
defmodule DbMnesia do
…
def delete( table_name, wheres ) do
:mnesia.start
table_atom = table_name |> String.to_atom
:mnesia.wait_for_tables( [ table_atom ], 1000 )
where_spec = wheres |> wheres_value |> Tuple.insert_at( 0, table_atom )
result = :mnesia.transaction( fn -> :mnesia.delete( where_spec ) end )
case result do
{ :atomic, :ok } -> { :ok }
{ :aborted, err } -> { :error, err }
{ _, err } -> { :error, err }
err -> { :error, err }
end
end
…
2)create.json.eex内でDBデータ追加
APIコントローラのcreate()に書いていたDBアクセッサの呼び出しを、create.json.eexというJSONテンプレートを作成し、そこに移します
data = params[ "data" ]
Db.query( "insert into members values( '#{ data[ "name" ] }', #{ data[ "age" ] }, '#{ data[ "team" ] }', '#{ data[ "position" ] }' )" )
3)delete.json.eex内でDBデータ削除
APIコントローラのdelete()に書いていたDBアクセッサの呼び出しも、delete.json.eexというJSONテンプレートを作成し、そこに移します
Db.query( "delete from members where id = #{ params[ "id" ] }" )
手順③:動作確認
1)追加REST APIの動作確認
まずは追加の確認からいきます
RESTクライアントで、idを削除した下記のリクエストbodyを設定します
{
"data":
{
"name": "テストユーザ125",
"age": 48,
"team": "障害対応チーム",
"position": "バグ改修担当"
}
}
「POST http://localhost:4000/api/v1/users」
で追加が行われ、id=7のデータが追加されれば成功です
「GET http://localhost:4000/api/v1/users」
で一覧し、id=7の追加を確認します
2)削除REST APIの動作確認
次に、削除の確認です
「DELETE http://localhost:4000/api/v1/users/7」
で削除を行います
「GET http://localhost:4000/api/v1/users」
で一覧し、id=7が削除されたことを確認します
終わり
JSONテンプレートによるDB操作で実装された、軽量API版の追加・削除REST APIを実装しました
各種JSONテンプレートを配置するだけで、REST APIが実装されていくことが、だんだん現実味を帯びてきたのでは無いでしょうか?
最終的なコードは、以下にまとめてあります
Phoenix軽量APIを使ったREST APIのコード(データはMnesia DBに保持)
https://qiita.com/piacerex/items/a41176fb0c7c68523a4b
今回の内容が上手く理解できなかった方は、上記からコラムを追って、まず手元で動かすところから始めてみてください
次回は、いよいよREST APIのラスト、更新REST APIを実装します