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?

ElixirDesktopで作るブログアプリ APIサーバー 主キーのULID化、リレーション

Posted at

はじめに

この記事はElixirアドベントカレンダー2025シリーズ2の16日目の記事です。

今回はセッション管理を予定していましたが
ULID化とユーザーのポストのリレーション、共通エラー処理の実装を行います

主キーをULIDにする

まだデータを入れたりしてないので、IDを整数のauto incrementからULIDに変更します

参考

ULIDはソートできるしuuidv7より容量少ないしスケールした際(しないけど)に良さそうなのでこちらを使用します

参考記事の最後の方にありますが、ULIDはIDが推測できたりするので請求書とかセンシティブなデータだけはUUIDv4とかにすると良いみたいですね

ULIDセットアップ

こちらを追加います

mix.exs
  defp deps do
    [
-     {:bandit, "~> 1.5"}    
+     {:bandit, "~> 1.5"},
+     {:ecto_ulid_next, "~> 1.0.2"}
    ]
  end
mix deps.get

マイグレーションファイルに手を加えずに全体設定として idをbinary_id、外部キーもbinary_idにします

config/config.exs:L23
config :blog,
  ecto_repos: [Blog.Repo],
  generators: [timestamp_type: :utc_datetime]

+ config :blog,
+      Blog.Repo,
+      migration_primary_key: [name: :id, type: :binary_id],
+      migration_foreign_key: [type: :binary_id]

主キーの形式をUILDで作るようにします
UUIDv4やv7使いたいときはここを変えるだけでテーブルごとに使うIDを指定できます

lib/blog/accounts/user.ex
defmodule Blog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

+ @primary_key {:id, Ecto.ULID, autogenerate: true}
+ @foreign_key_type Ecto.ULID

lib/blog/accounts/user_token.ex
defmodule Blog.Accounts.UserToken do
  use Ecto.Schema
  import Ecto.Query
  alias Blog.Accounts.UserToken

+ @primary_key {:id, Ecto.ULID, autogenerate: true}
+ @foreign_key_type Ecto.ULID

  @hash_algorithm :sha256
  @rand_size 32

primary_keyの設定したらDBをリセットしておきましょう

mix ecto.reset

これで主キーがULIDになりました

API関連ファイルを作成

認証周りを実装する前にAPIのエラーレスポンスを返す処理群がほしいので先にAPIを作成します

--webオプションでcontrollerを api/v1配下に置いてモジュール名もApi.V1.[コントローラー]にしてくれます

mix phx.gen.json Posts Post posts text:string --web Api.V1

phoenix1.8からphx.gen.authをしてると自動でユーザーとの関連をつけてくれるのですが、
idをbinary_idに変えたのでここを修正します

priv/repo/migrations/20251217131050_create_posts.exs
defmodule Blog.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:posts) do
      add :text, :string
-     add :user_id, references(:users, type: :id, on_delete: :delete_all)
+     add :user_id, references(:users, on_delete: :delete_all)

      timestamps(type: :utc_datetime)
    end

    create index(:posts, [:user_id])
  end
end

スキーマファイルもULID対応にします

lib/blog/posts/post.ex
defmodule Blog.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, Blog.Accounts.User

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(post, attrs, user_scope) do
    post
    |> cast(attrs, [:text])
    |> validate_required([:text])
    |> put_change(:user_id, user_scope.user.id)
  end
end

修正したらマイグレーションを実行します

mix ecto.migrate

これでpost周りは一旦置いといて

上記のコマンドで追加で以下のファイルが作成されます
これを使用することで、APIでエラーがあった時にエラー内容に応じでレスポンスを返してくれます
ここに認証エラーとそれサーバーエラーを足していきます

lib/blog_web/controllers/fallback_controller.ex
defmodule BlogWeb.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.

  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use BlogWeb, :controller

  # This clause handles errors returned by Ecto's insert/update/delete.
  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(json: BlogWeb.ChangesetJSON)
    |> render(:error, changeset: changeset)
  end

  # This clause is an example of how to handle resources that cannot be found.
  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(html: BlogWeb.ErrorHTML, json: BlogWeb.ErrorJSON)
    |> render(:"404")
  end

+ def call(conn, {:error, :unauthorized}) do
+   conn
+   |> put_status(:unauthorized)
+   |> put_view(html: BlogWeb.ErrorHTML, json: BlogWeb.ErrorJSON)
+   |> render(:"401")
+ end  
+
+ def call(conn, {:error, reason}) do
+   conn
+   |> put_status(:internal_server_error)
+   |> put_view(html: BlogWeb.ErrorHTML, json: BlogWeb.ErrorJSON)
+   |> render(:error, %{detail: reason})
+ end
end

最後に

Phoenix1.8になってからリレーション周りまでいい感じにやってくれて細かいところまで行き届いてすごいなぁと思う限りでした

長いので一旦ここまでとし、次の記事でユーザー登録APIとトークン認証をやっていきます

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

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?