name: ecto-patterns
description: Ecto模式用于模式、查询、变更集和迁移。在编写数据库代码时加载。
user-invocable: false
Ecto模式参考
用于处理Ecto模式、查询和迁移的参考。
铁律 — 绝不违反
- 变更集用于外部数据 — 使用
cast/4 处理用户/API输入,change/2 或 put_change/3 处理内部可信数据
- 永远不要用
:float 表示金钱 — 始终使用 :decimal 或 :integer(以分为单位)
- 不要使用Rails风格的多态关联 — 它们破坏外键约束;使用多个可空外键或单独的连接表
- 始终在查询中固定值 —
u.name == ^user_input 是安全的,字符串插值会导致SQL注入
- 预加载集合,而非单个对象 — 在循环中预加载会导致N+1查询问题
- 约束优于验证以防止竞态条件 — 验证提供快速反馈,约束提供数据库级别的安全性
has_many 使用单独查询,belongs_to 使用JOIN — 避免行乘法
- 不要使用隐式交叉连接 —
from(a in A, b in B) 没有 on: 会导致笛卡尔积
- 在
cast_assoc 前对共享数据去重 — 当多个父级共享子数据时,在构建变更集前对子记录去重。去重仅在单个变更集内有效
快速模式模板
defmodule MyApp.Context.Entity do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "entities" do
field :name, :string
field :status, Ecto.Enum, values: [:draft, :active, :archived]
field :amount_cents, :integer # 永远不要用:float表示金钱!
belongs_to :user, MyApp.Accounts.User
timestamps(type: :utc_datetime_usec)
end
def changeset(entity, attrs) do
entity
|> cast(attrs, [:name, :status, :amount_cents])
|> validate_required([:name])
|> foreign_key_constraint(:user_id)
end
end
快速决策
cast vs put_change vs change
| 函数 |
使用时机 |
cast/4 |
外部数据(用户输入,API) |
put_change/3 |
内部可信数据(时间戳,计算值) |
change/2 |
来自现有结构的内部数据 |
预加载策略
| 关系 |
策略 |
belongs_to |
JOIN(单个查询) |
has_many |
单独查询(避免行乘法) |
常见反模式
| 错误 |
正确 |
field :amount, :float |
field :amount_cents, :integer |
"SELECT * WHERE name = '#{name}'" |
from(u in User, where: u.name == ^name) |
Repo.all(User) |> Enum.filter(& &1.active) |
from(u in User, where: u.active) |
| 在循环中预加载 |
Repo.preload(posts, :comments) |
Repo.get!(User, user_id) 使用用户输入 |
Repo.get(User, id) + 处理nil |
参考
详细模式请参阅:
references/changesets.md - cast vs put_change,自定义验证,prepare_changes
references/queries.md - 可组合查询,动态查询,子查询,预加载
references/migrations.md - 安全迁移,并发索引,NOT NULL
references/transactions.md - Repo.transact,Ecto.Multi,upserts