9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

この記事は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に追加します

lib/blog_app_web/router.ex
  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指定します

lib/blog_app/posts/post.ex
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

トップページ差し替え

lib/blog_app_web/controllers/page_controller.ex
  def redirect_to(conn, %Accounts.Scope{}) do
-   redirect(conn, to: ~p"/welcome")
+   redirect(conn, to: ~p"/posts")
  end
lib/blog_app_web/controllers/user_session_controller.ex
  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モジュールを読み込みます

lib/blog_app/posts.ex
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への変換やリレーション周りのデータもいい感じにしてくれます

lib/blog_app/posts.ex
  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で構造体に変換します
取得できなかったりエラーが発生したら[]を返します

lib/blog_app/posts.ex
  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を発生させます

lib/blog_app/posts.ex
  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形式にしてフォームに反映させます
それ以外のエラーはそのまま返しています

lib/blog_app/posts.ex
  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でリアルタイムで更新しています

lib/blog_app/posts.ex
  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で一覧から対象の表示を削除しています

lib/blog_app/posts.ex
  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ができるのが確認できました

2b531014c06989188c4d28d4c843c773.gif

最後に

認証周りまでは結構手間ですが、それ以降は結構楽にできるのではないかなと思います

confirmのダイアログはiOS,Androidで出すには面倒なので次はUIの修正でそこあたりを解消しようかと思います

本記事は以上になりますありがとうございました

9
0
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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?