mcpField
E
ecto-patterns

ecto-patterns

github.com/bobmatnyc開発78340 回インストール15 週間前に更新
リポジトリ

ecto-patterns について

ecto-patterns は、Phoenix/Elixir 開発者向けの Ecto データレイヤー実践ガイドです。スキーマ、マイグレーション、クエリ、トランザクション、マルチテナンシー、テストを扱い、コンテキストへのドメインロジック配置とデータベース制約を重視します。

主な機能

  1. 01スキーマと changeset の検証
  2. 02安全なスキーマ・データ移行
  3. 03クエリ、プリロード、ページネーション
  4. 04トランザクションと Ecto.Multi
  5. 05マルチテナントのデータ分離
  6. 06データベーステストと性能の指針

活用シーン

  1. 01Phoenix アプリのデータレイヤー設計
  2. 02大規模なデータベーステーブルの安全な変更
  3. 03関連データ取得時の N+1 回避
  4. 04複数段階の書き込みとテナント別クエリの実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill ecto-patterns

インストール後、Claude Code で/ecto-patternsを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Phoenix/Elixir 向け Ecto のパターン

Ecto は Phoenix アプリケーションのデータレイヤーです。スキーマ、チェンジセット、クエリ、マイグレーション、トランザクションを扱います。Ecto を適切に使うことで、ドメインロジックをコンテキストに置き、データベースで制約を適用し、複数ステップのワークフローでトランザクションを使用できます。

スキーマとチェンジセット

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :hashed_password, :string
    field :confirmed_at, :naive_datetime
    has_many :memberships, MyApp.Orgs.Membership
    timestamps()
  end

  def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password])
    |> validate_required([:email, :password])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:password, min: 12)
    |> unique_constraint(:email)
    |> hash_password()
  end

  defp hash_password(%{valid?: true} = cs),
    do: put_change(cs, :hashed_password, Argon2.hash_pwd_salt(get_change(cs, :password)))
  defp hash_password(cs), do: cs
end

ガイドライン

  • キャストとバリデーションはチェンジセットに置き、ビジネスロジックはコンテキストに置きます。
  • バリデーションには必ず DB 制約(unique_constraint、foreign_key_constraint)を組み合わせます。
  • 更新には changeset/2 を使い、キャストせずに一括代入することは避けます。

マイグレーション

def change do
  create table(:users) do
    add :email, :citext, null: false
    add :hashed_password, :string, null: false
    add :confirmed_at, :naive_datetime
    timestamps()
  end

  create unique_index(:users, [:email])
end

安全なマイグレーションのヒント

  • 追加型の変更を優先します。列を NULL 許容で追加し、データをバックフィルしてから null: false を適用します。
  • 大きなテーブルでは、インデックスに concurrently: true を使います。Postgres では change 内で無効にし、up/down でラップします。
  • データマイグレーションは、execute/1 を介して mix ecto.migrate から呼び出す別モジュール、または独立したスクリプトにします。冪等性を確保してください。
  • ロックを考慮し、長いトランザクションは避けて、マイグレーションを小さなステップに分けます。

クエリとプリロード

import Ecto.Query

def list_users(opts \\ %{}) do
  base =
    from u in MyApp.Accounts.User,
      preload: [:memberships],
      order_by: [desc: u.inserted_at]

  Repo.all(apply_pagination(base, opts))
end

defp apply_pagination(query, %{limit: limit, offset: offset}),
  do: query |> limit(^limit) |> offset(^offset)
defp apply_pagination(query, _), do: query

パターン

  • ループ内で Repo を呼び出すのではなく、preload を使います。取得後に Repo.preload/2 を使う方法を優先します。
  • 大きな blob の読み込みを避けるには select を使います。
  • 並行処理では、行レベルロックが必要なクエリに lock: "FOR UPDATE" を指定し、Repo.transaction を使います。

トランザクションと Ecto.Multi

alias Ecto.Multi

def onboard_user(attrs) do
  Multi.new()
  |> Multi.insert(:user, User.registration_changeset(%User{}, attrs))
  |> Multi.insert(:org, fn %{user: user} ->
    Org.changeset(%Org{}, %{owner_id: user.id, name: attrs["org_name"]})
  end)
  |> Multi.run(:welcome, fn _repo, %{user: user} ->
    MyApp.Mailer.deliver_welcome(user)
    {:ok, :sent}
  end)
  |> Repo.transaction()
end

ガイドライン

  • 失敗する可能性のある副作用には Multi.run/3 を優先します。{:ok, value} または {:error, reason} を返します。
  • 一括更新には Multi.update_all を使い、無制限の書き込みを防ぐために where ガードを含めます。
  • エラーを上位へ伝播させ、コントローラーや LiveView で変換します。

アソシエーションと制約

  • ネストした変更を制御するには、on_replace: :delete/:nilify を使います。
  • DB エラーを適切に表面化させるため、チェンジセットで foreign_key_constraint/3 と unique_constraint/3 を定義します。
  • 多対多の関連でメタデータが必要な場合は、自動の many_to_many ではなく、結合スキーマ(has_many :memberships)を優先します。

ページネーションとフィルタリング

  • 小さなデータセットにはオフセット/リミットを、大きなリストにはカーソルベースの方式(Scrivener、Flop、Paginator)を使います。
  • コンテキストでフィルターを正規化し、コントローラーから直接クエリを組み立てないようにします。
  • フィルター対象の列に合わせて複合インデックスを追加し、EXPLAIN ANALYZE で確認します。

マルチテナントのパターン

  • プレフィックスベース:テナントごとに Postgres スキーマを使用します(prefix: を指定した put_source/2)。分離性に優れますが、テナントごとのマイグレーションが必要です。
  • 行ベース:tenant_id 列と行フィルターを使用します。マイグレーションはより簡単ですが、大規模な場合はテナントごとに部分インデックスを追加します。
  • コンテキストでは常にテナントでクエリをスコープし、適用を徹底するためにポリシーやガードの使用を検討します。

パフォーマンスと運用

  • 大規模なエクスポートには Repo.stream を使い、Repo.transaction でラップします。
  • 頻繁に読み取られるデータは ETS/Cachex でキャッシュし、書き込み時に無効化します。
  • LiveView/Channels のクエリ数を監視し、N+1 を避けるためにレンダリング前にプリロードします。
  • テレメトリー:OpentelemetryEcto はクエリ時間をエクスポートします。DB 接続プールのメトリクスも追加します。

テスト

use MyApp.DataCase, async: true

test "registration changeset validates email" do
  changeset = User.registration_changeset(%User{}, %{email: "bad", password: "secretsecret"})
  refute changeset.valid?
  assert %{email: ["has invalid format"]} = errors_on(changeset)
end
  • DataCase はサンドボックス化された DB をセットアップします。トランザクションが競合しない限り、テストは非同期のままにします。
  • test/support のファクトリー/フィクスチャを使って、有効な構造体をすばやく構築します。
  • マイグレーションでは、制約(ユニーク制約/インデックスに基づく制約)の回帰テストを追加します。

よくある落とし穴

  • リスクの高い DDL を 1 回のマイグレーションで実行する(ロックを避けるため、分割します)。
  • DB 制約を省略し、チェンジセットだけに頼る。
  • プリロードせず、ループ内でアソシエーションをクエリする。
  • 複数ステップの書き込みでトランザクションを使わない(失敗時に状態が部分的に反映される)。
  • マルチテナント構成で、読み取り/書き込み時にテナントのスコープを指定し忘れる。

コメント