自动化迁移
ent 迁移为数据库模式与根目录下 ent/migrate/schema.go 中定义的模式对象保持对齐而提供支持选项。
自动迁移
在应用初始化中运行自动迁移逻辑:
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
Create 创建 ent 项目所需的所有数据库资源。
默认情况下 Create 在 "append-only" 模式下工作,这意味着它只创建新的表和索引,追加列到表中或扩展列类型。
例如将 int 变更为 bigint。
那么如何丢弃列或索引呢?
丢弃资源
WithDropIndex 和 WithDropColumn 是丢弃表的列和索引的两个选项。
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(
ctx,
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
为在调试模式下运行迁移(打印所有 SQL 查询),运行:
err := client.Debug().Schema.Create(
ctx,
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
全球唯一ID
默认情况下,每个表的 SQL 主键从 1 开始,这意味着不同类型的多个实体共享相同的 ID。 这与 AWS Neptune 不同,AWS Neptune 的节点 ID 是 UUID。 参阅此处 了解如何在 SQL 数据库中使用 Ent 如何启动全球唯一 ID。
离线模式
随着 Atlas 很快会成为默认迁移引擎,离线迁移会被 版本化迁移所替代。
离线模式允许你在数据库上执行模式变更之前将其写入到 io.Writer 中。
这对于在数据库上执行之前验证 SQL 命令或获取 SQL 脚本来手工运行的情况下很有用。
打印变更
package main
import (
"context"
"log"
"os"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Dump migration changes to stdout.
if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
}
将变更写入到文件
package main
import (
"context"
"log"
"os"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Dump migration changes to an SQL script.
f, err := os.Create("migrate.sql")
if err != nil {
log.Fatalf("create migrate file: %v", err)
}
defer f.Close()
if err := client.Schema.WriteTo(ctx, f); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
}
外键
默认情况下, ent 使用外键来定义关系(边),以确保数据库的正确性和唯一性。
但是 ent 也通过使用 WithForeignKeys 选项来提供禁用此功能的选项。
你需要注意设置此选项为 false 会使迁移在模式 DDL 中不创建外键,并由开发者自身手动校验边并进行清理处理。
我们计划在近期提供一组钩子,用于在应用程序层实现外键约束。
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(
ctx,
migrate.WithForeignKeys(false), // Disable foreign keys.
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
迁移钩子
框架为迁移阶段提供了添加迁移钩子(中间件)的选项。 此选项非常适合用于修改或过滤迁移操作中涉及的表,或在数据库中创建自定义资源。
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"entgo.io/ent/dialect/sql/schema"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(
ctx,
schema.WithHooks(func(next schema.Creator) schema.Creator {
return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
// Run custom code here.
return next.Create(ctx, tables...)
})
}),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
Atlas 集成
从 v0.10 版本开始,Ent 支持使用 Atlas 执行迁移操作。
Atlas 是一个更强大的迁移框架,涵盖了当前 Ent 迁移包未支持的众多功能。
若要使用 Atlas 引擎执行迁移,请使用WithAtlas(true) 选项。
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"entgo.io/ent/dialect/sql/schema"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(ctx, schema.WithAtlas(true))
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
除了标准选项(例如WithDropColumn、WithGlobalUniqueID)外,Atlas 集成还提供了额外的选项,用于在模式迁移步骤中植入钩子。

Atlas Diff 和 Apply 钩子
以下是展示植入 Atlas Diff 和 Apply 两个步骤钩子的两个示例:
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"ariga.io/atlas/sql/migrate"
atlas "ariga.io/atlas/sql/schema"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err := client.Schema.Create(
ctx,
// Hook into Atlas Diff process.
schema.WithDiffHook(func(next schema.Differ) schema.Differ {
return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) {
// Before calculating changes.
changes, err := next.Diff(current, desired)
if err != nil {
return nil, err
}
// After diff, you can filter
// changes or return new ones.
return changes, nil
})
}),
// Hook into Atlas Apply process.
schema.WithApplyHook(func(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// Example to hook into the apply process, or implement
// a custom applier. For example, write to a file.
//
// for _, c := range plan.Changes {
// fmt.Printf("%s: %s", c.Comment, c.Cmd)
// if err := conn.Exec(ctx, c.Cmd, c.Args, nil); err != nil {
// return err
// }
// }
//
return next.Apply(ctx, conn, plan)
})
}),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
Diff 钩子示例
ent/schema 字段重命名场景下,Ent 不会将此变更探测为重命名,而是在差异比较阶段建议进行 DropColumn 和 AddColumn。
解决此问题的一种方法是在字段上使用 存储名称 选项在数据库表中保留旧列名。
不过使用 Atlas Diff 钩子允许将 DropColumn 和 AddColumn 变更替换为 RenameColumn 变更。
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
// ...
if err := client.Schema.Create(ctx, schema.WithDiffHook(renameColumnHook)); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
func renameColumnHook(next schema.Differ) schema.Differ {
return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) {
changes, err := next.Diff(current, desired)
if err != nil {
return nil, err
}
for _, c := range changes {
m, ok := c.(*atlas.ModifyTable)
// Skip if the change is not a ModifyTable,
// or if the table is not the "users" table.
if !ok || m.T.Name != user.Table {
continue
}
changes := atlas.Changes(m.Changes)
switch i, j := changes.IndexDropColumn("old_name"), changes.IndexAddColumn("new_name"); {
case i != -1 && j != -1:
// Append a new renaming change.
changes = append(changes, &atlas.RenameColumn{
From: changes[i].(*atlas.DropColumn).C,
To: changes[j].(*atlas.AddColumn).C,
})
// Remove the drop and add changes.
changes.RemoveIndex(i, j)
m.Changes = changes
case i != -1 || j != -1:
return nil, errors.New("old_name and new_name must be present or absent")
}
}
return changes, nil
})
}
Apply 钩子示例
Apply 钩子允许访问和修改迁移计划及其原始变更(SQL 语句),但除此之外它在计划应用前后执行自定义 SQL 语句也很有用。
例如,默认情况下不允许将一个可空列变更为没有默认值的不可空列。但是我们可以使用 Apply 钩子在此列中 UPDATE 所有包含 NULL 值的记录:
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
// ...
if err := client.Schema.Create(ctx, schema.WithApplyHook(fillNulls)); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
func fillNulls(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// There are three ways to UPDATE the NULL values to "Unknown" in this stage.
// Append a custom migrate.Change to the plan, execute an SQL statement directly
// on the dialect.ExecQuerier, or use the ent.Client used by the project.
// Execute a custom SQL statement.
query, args := sql.Dialect(dialect.MySQL).
Update(user.Table).
Set(user.FieldDropOptional, "Unknown").
Where(sql.IsNull(user.FieldDropOptional)).
Query()
if err := conn.Exec(ctx, query, args, nil); err != nil {
return err
}
// Append a custom statement to migrate.Plan.
//
// plan.Changes = append([]*migrate.Change{
// {
// Cmd: fmt.Sprintf("UPDATE users SET %[1]s = '%[2]s' WHERE %[1]s IS NULL", user.FieldDropOptional, "Unknown"),
// },
// }, plan.Changes...)
// Use the ent.Client used by the project.
//
// drv := sql.NewDriver(dialect.MySQL, sql.Conn{ExecQuerier: conn.(*sql.Tx)})
// if err := ent.NewClient(ent.Driver(drv)).
// User.
// Update().
// SetDropOptional("Unknown").
// Where(/* Add predicate to filter only rows with NULL values */).
// Exec(ctx); err != nil {
// return fmt.Errorf("fix default values to uppercase: %w", err)
// }
return next.Apply(ctx, conn, plan)
})
}