ecto-patterns by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill ecto-patternsEcto 是 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
指导原则
unique_constraint、foreign_key_constraint)配对使用。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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: false。concurrently: true;在 change 中禁用并在 up/down 中包装以用于 Postgres。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
模式
preload 而不是在循环中调用 Repo;获取后优先使用 Repo.preload/2。select 避免加载大型二进制对象。lock: "FOR UPDATE" 的 Repo.transaction。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 守卫以防止无限制写入。on_replace: :delete/:nilify 来控制嵌套更改。foreign_key_constraint/3 和 unique_constraint/3,以清晰地呈现数据库错误。has_many :memberships)而不是自动的 many_to_many。Scrivener、Flop、Paginator)。EXPLAIN ANALYZE 验证。put_source/2 并指定 prefix:)——隔离性好,需要每个租户的迁移。tenant_id 列 + 行过滤器——迁移更简单;当数据量大时,为每个租户添加部分索引。Repo.stream;包装在 Repo.transaction 中。OpentelemetryEcto 导出查询计时;添加数据库连接池指标。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 设置沙盒化数据库;除非事务冲突,否则保持测试异步。test/support 中使用工厂/固定数据快速构建有效的结构体。每周安装数
66
代码仓库
GitHub 星标数
18
首次出现
2026 年 1 月 23 日
安全审计
安装于
claude-code52
gemini-cli51
opencode50
codex50
cursor47
github-copilot47
Ecto is the data layer for Phoenix applications: schemas, changesets, queries, migrations, and transactions. Good Ecto practice keeps domain logic in contexts, enforces constraints in the database, and uses transactions for multi-step workflows.
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
Guidelines
unique_constraint, foreign_key_constraint).changeset/2 for updates; avoid mass assigning without casting.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
Safe migration tips
concurrently: true for indexes; disable in change and wrap in up/down for Postgres.mix ecto.migrate via execute/1 or in distinct scripts; ensure idempotence.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
Patterns
preload rather than calling Repo in loops; prefer Repo.preload/2 after fetching.select to avoid loading large blobs.Repo.transaction with lock: "FOR UPDATE" in queries that need row-level locks.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
Guidelines
Multi.run/3 for side effects that can fail; return {:ok, value} or {:error, reason}.Multi.update_all for batch updates; include where guards to prevent unbounded writes.on_replace: :delete/:nilify to control nested changes.foreign_key_constraint/3 and unique_constraint/3 in changesets to surface DB errors cleanly.has_many :memberships) instead of automatic many_to_many when you need metadata.Scrivener, Flop, Paginator).EXPLAIN ANALYZE.put_source/2 with prefix:) — good isolation, needs per-tenant migrations.tenant_id column + row filters — simpler migrations; add partial indexes per tenant when large.Repo.stream for large exports; wrap in Repo.transaction.OpentelemetryEcto exports query timings; add DB connection pool metrics.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 sets up sandboxed DB; keep tests async unless transactions conflict.test/support to build valid structs quickly.Weekly Installs
66
Repository
GitHub Stars
18
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code52
gemini-cli51
opencode50
codex50
cursor47
github-copilot47
Firestore 基础入门指南 - 配置、安全规则、SDK 使用与索引优化
1,300 周安装