跳到主要内容

自动迁移

ent 的迁移支持提供了使数据库模式与项目根目录下 ent/migrate/schema.go 中定义的模式对象保持一致的选项。

自动迁移

在应用初始化时运行自动迁移逻辑:

if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}

Create 会为您的 ent 项目创建所有必需的数据库资源。默认情况下,Create"仅追加" 模式工作;这意味着它只会创建新表和索引、向表追加列或扩展列类型。例如,将 int 改为 bigint

那么删除列或索引呢?

删除资源

WithDropIndexWithDropColumn 是用于删除表列和索引的两个选项。

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()
// 运行迁移。
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 不同,后者的节点 ID 是 UUID。阅读本文了解在使用 Ent 与 SQL 数据库时如何启用全局唯一 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()
// 将迁移更改输出到 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()
// 将迁移更改转储到 SQL 脚本。
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()
// 运行迁移。
err = client.Schema.Create(
ctx,
migrate.WithForeignKeys(false), // 禁用外键。
)
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()
// 运行迁移。
err = client.Schema.Create(
ctx,
schema.WithHooks(func(next schema.Creator) schema.Creator {
return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
// 在此处运行自定义代码。
return next.Create(ctx, tables...)
})
}),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

Atlas 集成

从 v0.10 开始,Ent 支持使用 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)/极速")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// 运行迁移。
err = client.Schema.Create(ctx, schema.WithAtlas(true))
if err != nil {
log.Fatalf("failed creating schema resources:极速
}
}

除了标准选项(例如 WithDropColumnWithGlobalUniqueID),Atlas 集成还提供了用于介入模式迁移步骤的附加选项。

atlas-migration-process

Atlas DiffApply 钩子

这里有两个示例展示了如何介入 Atlas DiffApply 步骤。

package main

import (
"context"
"log"

"<project>/ent"
"<project>/ent/migrate"

"ariga.io/atlas/sql/migrate"
atlas "ariga.io/atlas/s极速
"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()
// 运行迁移。
err := client.Schema.Create(
ctx,
// 介入 Atlas Diff 过程。
schema.WithDiffHook(func(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
}
// 差异之后,您可以过滤
// 更改或返回新更改。
return changes, nil
})
}),
// 介入 Atlas Apply 过程。
schema.WithApplyHook(func(next schema.Applier) schema.Applier {
return schema.Apply极速
// 介入应用过程的示例,或实现
// 自定义应用器。例如,写入文件。
//
// 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 next.Apply(ctx, conn, plan)
})
}),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

Diff 钩子示例

如果 ent/schema 中的字段被重命名,Ent 不会将此更改检测为重命名,并将在差异阶段提出 DropColumnAddColumn 更改。解决此问题的一种方法是在字段上使用 StorageKey 选项,并在数据库表中保留旧列名。但是,使用 Atlas Diff 钩子允许将 DropColumnAddColumn 更改为 RenameColumn 更改。

func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log极速
}
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)
// 如果更改不是 ModifyTable,
// 或者表不是 "users" 表,则跳过。
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:
// 追加一个新的重命名更改。
changes = append(changes, &atlas.RenameColumn{
From: changes[i].(*atlas.DropColumn).C,
To: changes[j].(*atlas.AddColumn).C,
})
// 移除删除和添加更改。
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极速
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// 在此阶段有三种方法可以将 NULL 值 UPDATE 为 "Unknown"。
// 向计划追加自定义 migrate.Change,直接在
// dialect.ExecQuerier 上执行 SQL 语句,或使用项目使用的 ent.Client。

// 执行自定义 SQL 语句。
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
}

// 向 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...)

// 使用项目使用的 ent.Client。
//
// drv := sql.NewDriver(dialect.MySQL, sql.Conn{ExecQuerier: conn.(*sql.Tx)})
// if err := ent.NewClient(ent.Driver(drv)).
// User.
// Update().
// SetDropOptional("Unknown").
// Where(/* 添加谓词以仅筛选具有 NULL 值的行 */).
// 极速
// return fmt.Errorf("fix default values to uppercase: %w", err)
// }

return next.Apply(ctx, conn, plan)
})
}