.Migration Template
defmodule MyApp.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users, primary_key: false) do
add :id, :binary_id, primary_key: true
add :email, :string, null: false
add :name, :string, null: false
add :role, :string, null: false, default: "member"
add :metadata, :map, default: %{}
add :tenant_id, :binary_id, null: false
add :team_id, references(:teams, type: :binary_id, on_delete: :delete_all)
timestamps(type: :utc_datetime_usec)
end
create unique_index(:users, [:tenant_id, :email])
create index(:users, [:tenant_id])
create index(:users, [:team_id])
end
end
Column Types
See references/column-types.md for complete type mapping and guidance.
Key decisions:
- IDs: Use
:binary_id (UUID) — set primary_key: false on table, add :id manually. - Money: Use
:integer (cents) or :decimal — never :float. - Timestamps: Always
timestamps(type: :utc_datetime_usec). - Enums: Use
:string with app-level Ecto.Enum — avoid Postgres enums (hard to migrate). - JSON: Use
:map (maps to jsonb). - Arrays: Use
{:array, :string} etc.
Index Strategies
See references/index-patterns.md for detailed index guidance.
When to Add Indexes
Always index:
- Foreign keys (
_id columns) tenant_id (first column in composite indexes)- Columns used in
WHERE clauses - Columns used in
ORDER BY - Unique constraints
Index Types
# Standard B-tree
create index(:users, [:tenant_id])
# Unique
create unique_index(:users, [:tenant_id, :email])
# Partial (conditional)
create index(:orders, [:status], where: "status != 'completed'", name: :orders_active_status_idx)
# GIN for JSONB
create index(:events, [:metadata], using: :gin)
# GIN for array columns
create index(:posts, [:tags], using: :gin)
# Composite
create index(:orders, [:tenant_id, :status, :inserted_at])
# Concurrent (no table lock — use in separate migration)
@disable_ddl_transaction true
@disable_migration_lock true
def change do
create index(:users, [:email], concurrently: true)
end
Constraints
# Check constraint
create constraint(:orders, :amount_must_be_positive, check: "amount > 0")
# Exclusion constraint (requires btree_gist extension)
execute "CREATE EXTENSION IF NOT EXISTS btree_gist", ""
create constraint(:reservations, :no_overlapping_bookings,
exclude: ~s|gist (room_id WITH =, tstzrange(starts_at, ends_at) WITH &&)|
)
# Unique constraint (same as unique_index for most purposes)
create unique_index(:accounts, [:slug])
References (Foreign Keys)
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
add :team_id, references(:teams, type: :binary_id, on_delete: :nilify_all)
add :parent_id, references(:categories, type: :binary_id, on_delete: :nothing)
on_delete | Use When |
|---|
| ------------- | ---------- |
:delete_all | Child can't exist without parent (memberships, line items) |
:nilify_all | Child should survive parent deletion (optional association) |
:nothing | Handle in application code (default) |
:restrict | Prevent parent deletion if children exist |
Multi-Tenant Patterns
Every Table Gets tenant_id
def change do
create table(:items, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :tenant_id, :binary_id, null: false
timestamps(type: :utc_datetime_usec)
end
# Always composite index with tenant_id first
create index(:items, [:tenant_id])
create unique_index(:items, [:tenant_id, :name])
end
Adding tenant_id to Existing Tables
def change do
alter table(:items) do
add :tenant_id, :binary_id
end
# Backfill in a separate data migration, then:
# alter table(:items) do
# modify :tenant_id, :binary_id, null: false
# end
end
Data Migrations
Rule: Never mix schema changes and data changes in the same migration.
Safe Data Migration Pattern
defmodule MyApp.Repo.Migrations.BackfillUserRoles do
use Ecto.Migration
# Don't use schema modules — they may change after this migration runs
def up do
execute """
UPDATE users SET role = 'member' WHERE role IS NULL
"""
end
def down do
# Data migrations may not be reversible
:ok
end
end
Batched Data Migration (large tables)
def up do
execute """
UPDATE users SET role = 'member'
WHERE id IN (
SELECT id FROM users WHERE role IS NULL LIMIT 10000
)
"""
# For very large tables, use a Task or Oban job instead
end
Reversible vs Irreversible
Reversible (use change)
These are auto-reversible:
create table ↔ drop tableadd column ↔ remove columncreate index ↔ drop indexrename ↔ rename
Irreversible (use up/down)
Must define both directions:
modify column type — Ecto can't infer the old typeexecute raw SQL- Data backfills
- Dropping columns with data
def up do
alter table(:users) do
modify :email, :citext, from: :string # from: helps reversibility
end
end
def down do
alter table(:users) do
modify :email, :string, from: :citext
end
end
Using modify with from:
Phoenix 1.7+ supports from: for reversible modify:
def change do
alter table(:users) do
modify :email, :citext, null: false, from: {:string, null: true}
end
end
PostgreSQL Extensions
def change do
execute "CREATE EXTENSION IF NOT EXISTS citext", "DROP EXTENSION IF EXISTS citext"
execute "CREATE EXTENSION IF NOT EXISTS pgcrypto", "DROP EXTENSION IF EXISTS pgcrypto"
execute "CREATE EXTENSION IF NOT EXISTS pg_trgm", "DROP EXTENSION IF EXISTS pg_trgm"
end
Enum Types (PostgreSQL native — use sparingly)
Prefer Ecto.Enum with :string columns. If you must use Postgres enums:
def up do
execute "CREATE TYPE order_status AS ENUM ('pending', 'confirmed', 'shipped', 'delivered')"
alter table(:orders) do
add :status, :order_status, null: false, default: "pending"
end
end
def down do
alter table(:orders) do
remove :status
end
execute "DROP TYPE order_status"
end
Warning: Adding values to Postgres enums requires ALTER TYPE ... ADD VALUE which cannot run inside a transaction. Prefer :string + Ecto.Enum.
Checklist
- [ ] Primary key:
primary_key: false + add :id, :binary_id, primary_key: true - [ ]
null: false on required columns - [ ]
timestamps(type: :utc_datetime_usec) - [ ] Foreign keys with appropriate
on_delete - [ ] Index on every foreign key column
- [ ]
tenant_id indexed (composite with lookup fields) - [ ] Unique constraints where needed
- [ ] Concurrent indexes in separate migration with
@disable_ddl_transaction true - [ ] Data migrations in separate files from schema migrations
版本历史
共 1 个版本
-
v1.0.0
当前
2026-03-28 16:58 安全 安全
安全检测
腾讯云安全 (Sanbu)
安全,无风险
查看报告
🔗 相关推荐
developer-tools
gchapim
浏览和搜索 Hacker News。获取热门、最新、最佳、Ask HN、Show HN 故事及招聘信息。查看条目详情、评论和用户资料。通过 Algolia 搜索故事和评论。查找“Who is hiring?”帖子。适用于任何 HN 相关查询
★ 10
📥 4,342
ai-intelligence
ivangdavila
自我反思+自我批评+自我学习+自组织记忆。智能体评估自身工作、发现错误并持续改进。
★ 1,358
📥 318,376
ai-intelligence
halthelobster
将AI智能体从任务执行者升级为主动预判需求、持续优化的智能伙伴。集成WAL协议、工作缓冲区、自主定时任务及实战验证模式。Hal Stack核心组件 🦞
★ 836
📥 213,138