name: ecto-migrator
description: "根据自然语言或模式描述生成 Ecto 迁移。支持处理表、列、索引、约束、引用、枚举和分区。支持可逆迁移、数据迁移和多租户模式。适用于在 Elixir 项目中创建或修改数据库模式、添加索引、更改表、创建枚举或执行数据迁移。"
解析用户的描述并生成迁移文件。常见模式:
| 用户描述 | 迁移操作 |
|---|---|
| "创建包含邮箱和姓名的用户表" | create table(:users) 并添加列 |
| "为用户表添加电话字段" | alter table(:users), add :phone |
| "为用户表的邮箱字段添加唯一性约束" | create unique_index(:users, [:email]) |
| "为所有表添加租户ID字段" | 多个 alter table 并添加索引 |
| "将订单表中的状态字段重命名为state" | rename table(:orders), :status, to: :state |
| "从用户表中移除 legacy_id 列" | alter table(:users), remove :legacy_id |
| "为订单表的金额字段添加大于0的检查约束" | create constraint(:orders, ...) |
mix ecto.gen.migration <名称>
# 生成: priv/repo/migrations/YYYYMMDDHHMMSS_<名称>.exs
命名约定:create_<表名>、add_<列名>_to_<表名>、create_<表名>_<列名>_index、alter_<表名>_add_<列名>。
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
完整的类型映射和指导请参阅 references/column-types.md。
关键决策:
- ID:使用 :binary_id (UUID) — 在表上设置 primary_key: false,手动添加 :id 列。
- 金额:使用 :integer (分) 或 :decimal — 切勿使用 :float。
- 时间戳:始终使用 timestamps(type: :utc_datetime_usec)。
- 枚举:使用 :string 配合应用层的 Ecto.Enum — 避免使用 PostgreSQL 原生枚举(难以迁移)。
- JSON:使用 :map (映射为 jsonb)。
- 数组:使用 {:array, :string} 等。
详细的索引指导请参阅 references/index-patterns.md。
始终为以下情况添加索引:
- 外键列 (_id 列)
- tenant_id (复合索引中的第一列)
- 在 WHERE 子句中使用的列
- 在 ORDER BY 子句中使用的列
- 唯一约束列
# 标准 B-树索引
create index(:users, [:tenant_id])
# 唯一索引
create unique_index(:users, [:tenant_id, :email])
# 部分索引(条件索引)
create index(:orders, [:status], where: "status != 'completed'", name: :orders_active_status_idx)
# GIN 索引用于 JSONB
create index(:events, [:metadata], using: :gin)
# GIN 索引用于数组列
create index(:posts, [:tags], using: :gin)
# 复合索引
create index(:orders, [:tenant_id, :status, :inserted_at])
# 并发索引(无表锁 — 在单独的迁移中使用)
@disable_ddl_transaction true
@disable_migration_lock true
def change do
create index(:users, [:email], concurrently: true)
end
# 检查约束
create constraint(:orders, :amount_must_be_positive, check: "amount > 0")
# 排除约束(需要 btree_gist 扩展)
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_index 作用相同)
create unique_index(:accounts, [:slug])
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 |
适用场景 |
|---|---|
:delete_all |
子记录不能脱离父记录存在(例如成员关系、订单项) |
:nilify_all |
父记录删除后子记录应保留(可选关联) |
:nothing |
在应用代码中处理(默认值) |
:restrict |
如果存在子记录,则阻止父记录删除 |
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
# 始终创建以 tenant_id 为首的复合索引
create index(:items, [:tenant_id])
create unique_index(:items, [:tenant_id, :name])
end
def change do
alter table(:items) do
add :tenant_id, :binary_id
end
# 在单独的数据迁移中回填数据,然后:
# alter table(:items) do
# modify :tenant_id, :binary_id, null: false
# end
end
规则:切勿在同一迁移中混合模式变更和数据变更。
defmodule MyApp.Repo.Migrations.BackfillUserRoles do
use Ecto.Migration
# 不要使用模式模块 — 它们在此迁移运行后可能会改变
def up do
execute """
UPDATE users SET role = 'member' WHERE role IS NULL
"""
end
def down do
# 数据迁移可能不可逆
:ok
end
end
def up do
execute """
UPDATE users SET role = 'member'
WHERE id IN (
SELECT id FROM users WHERE role IS NULL LIMIT 10000
)
"""
# 对于非常大的表,请改用 Task 或 Oban 作业
end
change)以下操作可自动逆转:
- create table ↔ drop table
- add column ↔ remove column
- create index ↔ drop index
- rename ↔ rename
up/down)必须明确定义两个方向的操作:
- modify 列类型 — Ecto 无法推断旧类型
- execute 原始 SQL
- 数据回填
- 删除包含数据的列
def up do
alter table(:users) do
modify :email, :citext, from: :string # from: 有助于实现可逆性
end
end
def down do
alter table(:users) do
modify :email, :string, from: :citext
end
end
from: 的 modifyPhoenix 1.7+ 支持 from: 以实现可逆的 modify:
def change do
alter table(:users) do
modify :email, :citext, null: false, from: {:string, null: true}
end
end
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
优先使用 :string 列配合 Ecto.Enum。如果必须使用 PostgreSQL 原生枚举:
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
警告: 向 PostgreSQL 枚举添加值需要使用 ALTER TYPE ... ADD VALUE,这无法在事务内运行。优先选择 :string + Ecto.Enum。
primary_key: false + add :id, :binary_id, primary_key: truenull: falsetimestamps(type: :utc_datetime_usec)on_delete 选项tenant_id 创建索引(与查询字段组成复合索引)@disable_ddl_transaction true