はじめに
この記事はElixirアドベントカレンダー2025シリーズ2の16日目の記事です。
今回はセッション管理を予定していましたが
ULID化とユーザーのポストのリレーション、共通エラー処理の実装を行います
主キーをULIDにする
まだデータを入れたりしてないので、IDを整数のauto incrementからULIDに変更します
参考
ULIDはソートできるしuuidv7より容量少ないしスケールした際(しないけど)に良さそうなのでこちらを使用します
参考記事の最後の方にありますが、ULIDはIDが推測できたりするので請求書とかセンシティブなデータだけはUUIDv4とかにすると良いみたいですね
ULIDセットアップ
こちらを追加います
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 :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を指定できます
defmodule Blog.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
+ @primary_key {:id, Ecto.ULID, autogenerate: true}
+ @foreign_key_type Ecto.ULID
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に変えたのでここを修正します
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対応にします
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でエラーがあった時にエラー内容に応じでレスポンスを返してくれます
ここに認証エラーとそれサーバーエラーを足していきます
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とトークン認証をやっていきます
本記事は以上になりますありがとうございました