Phoenixで作るGPS Logging System 4 APIの実装


はじめに

ひとりLiveView Advent Calendar の4日目の記事です

この記事はElixir Conf US 2021発表したシステムの構築と関連技術の解説を目的とした記事です

今回は以下の4つを実装します

  • phx.gen.jsonでAPI作成
  • phx.gen.schemaでモデルのみ作成
  • 手動でAPI作成
  • routerにエンドポイント追加

phx.gen.json

以下のコマンドでAPIとモデル、クエリーを作成しますが、一日目でphx.gen.liveで作ったので、
--no-schema --no-contextでAPIのみ作成しています

mix phx.gen.json Loggers Map maps name:string description:string --no-schema --no-context 
* creating lib/live_logger_web/controllers/map_controller.ex
* creating lib/live_logger_web/views/map_view.ex
* creating test/live_logger_web/controllers/map_controller_test.exs
* creating lib/live_logger_web/views/changeset_view.ex
* creating lib/live_logger_web/controllers/fallback_controller.ex

Add the resource to your :api scope in lib/live_logger_web/router.ex:

    resources "/maps", MapController, except: [:new, :edit]

作成されたファイルを見てみましょう

  • lib/live_logger_web/controllers/map_controller.ex
    CRUD APIが書かれています
  • lib/live_logger_web/views/map_view.ex
    APIのレスポンスを記述します controllerとviewファイルはセットなので忘れないようにしましょう
  • lib/live_logger_web/controllers/fallback_controller.ex
    action_fallbackで指定されていて各APIのエラー時にfallback_controllerのエラーにハンドリングされます
  • lib/live_logger_web/views/changeset_view.ex
    422 error時のレスポンスが記載されています
  • creating test/live_logger_web/controllers/map_controller_test.exs
    テストも自動生成やったぜ

phx.gen.schema

モデル単体だけを作成したい場合は phx.gen.schemaを実行します
クエリーも作成する場合は phx.gen.contextになります

mix phx.gen.schema Loggers.Point points lat:float lng:float device_id:string map_id:references:maps
mix ecto.migrate

モデルファイルとマイグレーションファイルのみ作成されます

* creating lib/live_logger/loggers/point.ex
* creating priv/repo/migrations/20211204133145_create_points.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate

migrateも忘れずに

mix ecto.migrate

PointのクエリーをLoggersに追加します
aliasで構造体をPointで呼び出せるようにして
list,createを実装します

query関数内で外部の引数を使用する場合はプリミティブな型でピン演算子を付ける必要があります
createはcreate_mapを参考に書きましょう

lib/live_logger/loggers.ex
defmodule LiveLogger.Loggers do
  ...
  alias LiveLogger.Loggers.Point
  ...
  def list_points(map_id) do
    Point
    |> where([p], p.map_id == ^map_id)
    |> Repo.all
  end

  def create_point(attrs \\ %{}) do
    %Point{}
    |> Point.changeset(attrs)
    |> Repo.insert
  end
end

リレーションも組んでいきます

lib/live_logger/loggers/map.ex
defmodule LiveLogger.Loggers.Map do
  use Ecto.Schema
  import Ecto.Changeset

  schema "maps" do
    field :description, :string
    field :name, :string

    belongs_to :user, LiveLogger.Accounts.User
    has_many :points, LiveLogger.Loggers.Point
    timestamps()
  end
  ...
end
lib/live_logger/loggers/point.ex
defmodule LiveLogger.Loggers.Point do
  use Ecto.Schema
  import Ecto.Changeset

  schema "points" do
    field :device_id, :string
    field :lat, :float
    field :lng, :float
    belongs_to :map, LiveLogger.Loggers.Map #field map_idを belongs_toに変更

    timestamps()
  end

  @doc false
  def changeset(point, attrs) do
    point
    |> cast(attrs, [:lat, :lng, :device_id, :map_id]) # map_id追加
    |> validate_required([:lat, :lng, :device_id, :map_id]) # map_id追加
  end
end

Map詳細でpointも取得するようにしましょう
preloadで関連先のデータを読み込みます

lib/live_logger/loggers.ex
defmodule LiveLogger.Loggers do
  ...
  def get_map_with_points!(id) do
    Map
    |> preload(:points)
    |> Repo.get!(id)
  end
  ...
end

手動でAPI作成

generatorなしで作りますが特に難しくありません
contextとモデルのモジュールをaliasで読み込む
action_fallbackを指定
apiを追加
レスポンスはsend_respのみでJSONは何も返さないためViewファイルはありません

lib/live_logger_web/controllers/point_controller.ex
defmodule LiveLoggerWeb.PointController do
  use LiveLoggerWeb, :controller

  alias LiveLogger.Loggers
  alias LiveLogger.Loggers.Point

  action_fallback LiveLoggerWeb.FallbackController

  def create(conn, point_params) do
    with {:ok, %Point{}} <- Loggers.create_point(point_params) do
      send_resp(conn, 200, "ok")
    end
  end
end

endpoint追加

routerのコメントアウトされている api scopeをコメントインして
コンソールに出てきてたroutingのコードを追加
pointsはcreateだけなので onlyで指定しています

lib/live_logger_web/router.ex
defmodule LiveLoggerWeb.Router do
  use LiveLoggerWeb, :router
  ...
  # Other scopes may use custom stacks.
  scope "/api", LiveLoggerWeb do
    pipe_through :api

    resources "/maps", MapController, except: [:new, :edit]
    resources  "/points", PointController, only: [:create]
  end
end

動作確認

動作確認はPostmanを使用します

get api/maps

get api/maps/1

post api/maps
createの引数が %{map: map_params} = params なので map[カラム名]でjson形式になるようにすること

delete api/maps/2
JSONのレスポンスはなく 204 No Contentが帰ってきています

create api/points
mapと違って point_paramsとしているのでフラットなformdataで大丈夫です

最後に

phx.gen.jsonによってAPIの実装も簡単にできました
json responseからdataが邪魔だなーと思ったら以下のようにすれば消すことができます

lib/live_logger_web/views/map_view.ex
defmodule LiveLoggerWeb.MapView do
  use LiveLoggerWeb, :view
  alias LiveLoggerWeb.MapView

  def render("index.json", %{maps: maps}) do
    render_many(maps, MapView, "map.json") # dataとMapの囲いを消す
  end
  ...
end

本記事は以上になります

次はAPIでの認証を実装します

Code