はじめに
この記事はElixirアドベントカレンダー2025シリーズ2の21日目の記事です。
今回はCRUDを作っていきます
phx.gen.live
CRUDの画面を作っていきます
mix phx.gen.live Posts Post posts text:string
作成後、マイグレーションファイルは削除してください
priv/repo/migrations/20251222145936_create_posts.exs
routerに追加します
scope "/", BlogAppWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{BlogAppWeb.UserAuth, :require_authenticated}] do
live "/users/settings", UserLive.Settings, :edit
live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
live "/welcome", UserLive.Welcome
+ live "/posts", PostLive.Index, :index
+ live "/posts/new", PostLive.Form, :new
+ live "/posts/:id", PostLive.Show, :show
+ live "/posts/:id/edit", PostLive.Form, :edit
end
post "/users/update-password", UserSessionController, :update_password
end
IDをULID指定します
defmodule BlogApp.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
+ @primary_key {:id, Ecto.ULID, autogenerate: true}
+ @foreign_key_type Ecto.ULID
schema "posts" do
field :text, :string
- field :user_id, :id
+ belongs_to :user, BlogApp.Accounts.User
timestamps(type: :utc_datetime)
end
トップページ差し替え
def redirect_to(conn, %Accounts.Scope{}) do
- redirect(conn, to: ~p"/welcome")
+ redirect(conn, to: ~p"/posts")
end
def guest(conn, %{}) do
case Accounts.register_user(%{id: Ecto.ULID.generate()}) do
{:ok, user} ->
conn
- |> put_session(:user_return_to, ~p"/welcome")
+ |> put_session(:user_return_to, ~p"/")
|> UserAuth.log_in_user(user)
{:error, _} ->
conn
|> put_flash(:error, "サーバーに接続できません")
|> redirect(to: ~p"/users/register")
end
end
API通信
DBに直接繋いでるのでCRUDはできますが、API経由で行うので書き換えます
使わない読み込みを消して、APIモジュールを読み込みます
defmodule BlogApp.Posts do
@moduledoc """
The Posts context.
"""
- import Ecto.Query, warn: false
- alias BlogApp.Repo
alias BlogApp.Posts.Post
alias BlogApp.Accounts.Scope
+ alias BlogApp.Api
JSONレスポンスをPost構造体に変換します
単純に構造体を作るのではなくてEcto.Changeset.apply_changes()を実行することでNaiveDatetimeへの変換やリレーション周りのデータもいい感じにしてくれます
def change_post(%Scope{} = scope, %Post{} = post, attrs \\ %{}) do
true = post.user_id == scope.user.id
Post.changeset(post, attrs, scope)
end
+ def convert_post(params, scope) do
+ Post.changeset(%Post{id: params["id"]}, params, scope)
+ |> Ecto.Changeset.apply_changes()
+ end
一覧は以下のように書き換えます
レスポンスのリストEnum.mapで構造体に変換します
取得できなかったりエラーが発生したら[]を返します
def list_posts(%Scope{} = scope) do
{:ok, resp} = Req.get(Api.client(), url: "/posts")
case resp.status do
200 ->
Enum.map(resp.body["data"], &convert_post(&1, scope))
_ ->
[]
end
end
1件取得になります
レスポンスを構造体に変換、エラーの場合は404を発生させます
def get_post!(%Scope{} = scope, id) do
{:ok, resp} = Req.get(Api.client(), url: "/posts/#{id}")
case resp.status do
200 ->
convert_post(resp.body["data"], scope)
_ ->
raise Ecto.NoResultsError
end
end
新規作成になります
json形式でデータを渡して
201の場合は構造体に変換してbroaddastしてviewを更新させます
422の場合はエラー内容をchangeset形式にしてフォームに反映させます
それ以外のエラーはそのまま返しています
def create_post(%Scope{} = scope, attrs) do
{:ok, resp} = Req.post(Api.client(), url: "/posts", json: %{post: attrs})
case resp.status do
201 ->
post = convert_post(resp.body["data"], scope)
broadcast_post(scope, {:created, post})
{:ok, post}
422 ->
%Post{}
|> Post.changeset(attrs, scope)
|> Api.assign_error(resp)
|> then(&{:error, &1})
_ ->
{:error, Post.changeset(%Post{}, attrs, scope)}
end
end
更新
新規作成とほぼ同じですね
実行者と更新するpostのuser_idが同一かをチェックしています
レスポンスを受け取ってbroad_castでリアルタイムで更新しています
def update_post(%Scope{} = scope, %Post{} = post, attrs) do
true = post.user_id == scope.user.id
{:ok, resp} =
Req.patch(Api.client(),
url: "/posts/#{post.id}",
json: %{post: Map.put(attrs, "id", post.id)}
)
case resp.status do
200 ->
post = convert_post(resp.body["data"], scope)
broadcast_post(scope, {:updated, post})
{:ok, post}
422 ->
%Post{}
|> Post.changeset(attrs, scope)
|> Api.assign_error(resp)
|> then(&{:error, &1})
_ ->
{:error, Post.changeset(%Post{}, attrs, scope)}
end
end
更新と同じで同一IDをチェックします
broad_castで一覧から対象の表示を削除しています
def delete_post(%Scope{} = scope, %Post{} = post) do
true = post.user_id == scope.user.id
{:ok, resp} =
Req.delete(Api.client(), url: "/posts/#{post.id}")
case resp.status do
204 ->
broadcast_post(scope, {:deleted, post})
{:ok, post}
_ ->
{:error, :not_found}
end
end
動作確認
APIでちゃんとCRUDができるのが確認できました
最後に
認証周りまでは結構手間ですが、それ以降は結構楽にできるのではないかなと思います
confirmのダイアログはiOS,Androidで出すには面倒なので次はUIの修正でそこあたりを解消しようかと思います
本記事は以上になりますありがとうございました
