6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-05

fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます :bow:

前作で追加・削除REST APIに対応し、今回の更新REST APIで、以下4つのHTTPメソッドが出揃い、いよいよ「軽量APIによるREST API」が完成です

本コラムの検証環境

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

あと下記コラムシリーズの続きとして実施するので、未実施であれば、事前に実施しておいてください(近々、ボイラープレート生成の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します

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

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)

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

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パターン)

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

4)ロジック実装し、テスト成功を確認(TDD Refactoring)

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

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

6)Dbモジュールにupdate処理を追加

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

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を作成します

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"にデータが入っている前提とします

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

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

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

image.png

「PUT http://localhost:4000/api/v1/users/6」でデータ更新が行われます
image.png

「GET http://localhost:4000/api/v1/users/6」を再度行うと、更新されたデータが表示されます
image.png

「GET http://localhost:4000/api/v1/users」でも、更新されたデータが確認できます
image.png

終わり

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

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

image.pngimage.png にて、どうぞ応援よろしくお願いします:bow:

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?