Automatic Migration

    在应用程序初始化过程中运行自动迁移逻辑:

    Create 创建你项目 ent 部分所需的的数据库资源 。 默认情况下,Create“append-only”模式工作;这意味着,它只创建新表和索引,将列追加到表或扩展列类型。 例如,将int改为bigint

    想要删除列或索引怎么办?

    删除资源

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

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "<project>/ent"
    6. "<project>/ent/migrate"
    7. )
    8. func main() {
    9. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    10. if err != nil {
    11. log.Fatalf("failed connecting to mysql: %v", err)
    12. }
    13. defer client.Close()
    14. ctx := context.Background()
    15. // Run migration.
    16. err = client.Schema.Create(
    17. ctx,
    18. migrate.WithDropIndex(true),
    19. migrate.WithDropColumn(true),
    20. )
    21. if err != nil {
    22. log.Fatalf("failed creating schema resources: %v", err)
    23. }
    24. }

    为了在调试模式下运行迁移 (打印所有SQL查询),请运行:

    1. err := client.Debug().Schema.Create(
    2. ctx,
    3. migrate.WithDropIndex(true),
    4. migrate.WithDropColumn(true),
    5. )
    6. if err != nil {
    7. log.Fatalf("failed creating schema resources: %v", err)
    8. }

    默认情况下,每个表的SQL主键从1开始;这意味着不同类型的多个实体可以有相同的ID。 不像AWS Neptune,节点ID是UUID。

    This does not work well if you work with , which requires the object ID to be unique.

    Versioned-migration users should follow the documentation when using WithGlobalUniqueID on MySQL 5.*. :::

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "<project>/ent"
    6. "<project>/ent/migrate"
    7. )
    8. func main() {
    9. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    10. if err != nil {
    11. log.Fatalf("failed connecting to mysql: %v", err)
    12. }
    13. defer client.Close()
    14. ctx := context.Background()
    15. // Run migration.
    16. if err := client.Schema.Create(ctx, migrate.WithGlobalUniqueID(true)); err != nil {
    17. log.Fatalf("failed creating schema resources: %v", err)
    18. }
    19. }

    How does it work? ent migration allocates a 1<<32 range for the IDs of each entity (table), and store this information in a table named ent_types. For example, type A will have the range of [1,4294967296) for its IDs, and type B will have the range of [4294967296,8589934592), etc.

    Note that if this option is enabled, the maximum number of possible tables is 65535.

    Offline Mode

    With Atlas becoming the default migration engine soon, offline migration will be replaced by versioned migrations.

    Offline mode allows you to write the schema changes to an io.Writer before executing them on the database. It’s useful for verifying the SQL commands before they’re executed on the database, or to get an SQL script to run manually.

    Print changes

    Write changes to a file

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "os"
    6. "<project>/ent"
    7. "<project>/ent/migrate"
    8. )
    9. func main() {
    10. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    11. if err != nil {
    12. log.Fatalf("failed connecting to mysql: %v", err)
    13. }
    14. defer client.Close()
    15. ctx := context.Background()
    16. // Dump migration changes to an SQL script.
    17. f, err := os.Create("migrate.sql")
    18. if err != nil {
    19. log.Fatalf("create migrate file: %v", err)
    20. }
    21. defer f.Close()
    22. if err := client.Schema.WriteTo(ctx, f); err != nil {
    23. log.Fatalf("failed printing schema changes: %v", err)
    24. }
    25. }

    However, ent also provide an option to disable this functionality using the WithForeignKeys option. You should note that setting this option to false, will tell the migration to not create foreign-keys in the schema DDL and the edges validation and clearing must be handled manually by the developer.

    We expect to provide a set of hooks for implementing the foreign-key constraints in the application level in the near future.

    1. package main
    2. import (
    3. "log"
    4. "<project>/ent/migrate"
    5. )
    6. func main() {
    7. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    8. if err != nil {
    9. log.Fatalf("failed connecting to mysql: %v", err)
    10. }
    11. defer client.Close()
    12. ctx := context.Background()
    13. // Run migration.
    14. err = client.Schema.Create(
    15. ctx,
    16. migrate.WithForeignKeys(false), // Disable foreign keys.
    17. )
    18. if err != nil {
    19. log.Fatalf("failed creating schema resources: %v", err)
    20. }
    21. }

    Migration Hooks

    The framework provides an option to add hooks (middlewares) to the migration phase. This option is ideal for modifying or filtering the tables that the migration is working on, or for creating custom resources in the database.

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "<project>/ent"
    6. "<project>/ent/migrate"
    7. "entgo.io/ent/dialect/sql/schema"
    8. )
    9. func main() {
    10. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    11. if err != nil {
    12. log.Fatalf("failed connecting to mysql: %v", err)
    13. }
    14. defer client.Close()
    15. ctx := context.Background()
    16. // Run migration.
    17. err = client.Schema.Create(
    18. ctx,
    19. schema.WithHooks(func(next schema.Creator) schema.Creator {
    20. return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
    21. // Run custom code here.
    22. return next.Create(ctx, tables...)
    23. })
    24. }),
    25. )
    26. if err != nil {
    27. log.Fatalf("failed creating schema resources: %v", err)
    28. }
    29. }

    Starting with v0.10, Ent supports running migration with Atlas, which is a more robust migration framework that covers many features that are not supported by current Ent migrate package. In order to execute a migration with the Atlas engine, use the WithAtlas(true) option.

    In addition to the standard options (e.g. WithDropColumn, WithGlobalUniqueID), the Atlas integration provides additional options for hooking into schema migration steps.

    atlas-migration-process

    Atlas Diff and Apply Hooks

    Here are two examples that show how to hook into the Atlas Diff and Apply steps.

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "<project>/ent"
    6. "<project>/ent/migrate"
    7. "ariga.io/atlas/sql/migrate"
    8. atlas "ariga.io/atlas/sql/schema"
    9. "entgo.io/ent/dialect"
    10. "entgo.io/ent/dialect/sql/schema"
    11. )
    12. func main() {
    13. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    14. if err != nil {
    15. log.Fatalf("failed connecting to mysql: %v", err)
    16. }
    17. defer client.Close()
    18. ctx := context.Background()
    19. // Run migration.
    20. err := client.Schema.Create(
    21. ctx,
    22. // Hook into Atlas Diff process.
    23. schema.WithDiffHook(func(next schema.Differ) schema.Differ {
    24. return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) {
    25. // Before calculating changes.
    26. changes, err := next.Diff(current, desired)
    27. if err != nil {
    28. return nil, err
    29. }
    30. // After diff, you can filter
    31. // changes or return new ones.
    32. return changes, nil
    33. })
    34. }),
    35. // Hook into Atlas Apply process.
    36. schema.WithApplyHook(func(next schema.Applier) schema.Applier {
    37. return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
    38. // Example to hook into the apply process, or implement
    39. // a custom applier. For example, write to a file.
    40. //
    41. // for _, c := range plan.Changes {
    42. // fmt.Printf("%s: %s", c.Comment, c.Cmd)
    43. // if err := conn.Exec(ctx, c.Cmd, c.Args, nil); err != nil {
    44. // return err
    45. // }
    46. //
    47. })
    48. }),
    49. )
    50. if err != nil {
    51. log.Fatalf("failed creating schema resources: %v", err)
    52. }
    53. }

    Diff Hook Example

    1. func main() {
    2. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    3. if err != nil {
    4. log.Fatalf("failed connecting to mysql: %v", err)
    5. }
    6. defer client.Close()
    7. // ...
    8. if err := client.Schema.Create(ctx, schema.WithDiffHook(renameColumnHook)); err != nil {
    9. log.Fatalf("failed creating schema resources: %v", err)
    10. }
    11. }
    12. func renameColumnHook(next schema.Differ) schema.Differ {
    13. return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) {
    14. changes, err := next.Diff(current, desired)
    15. if err != nil {
    16. return nil, err
    17. }
    18. for _, c := range changes {
    19. m, ok := c.(*atlas.ModifyTable)
    20. // Skip if the change is not a ModifyTable,
    21. // or if the table is not the "users" table.
    22. if !ok || m.T.Name != user.Table {
    23. continue
    24. }
    25. changes := atlas.Changes(m.Changes)
    26. switch i, j := changes.IndexDropColumn("old_name"), changes.IndexAddColumn("new_name"); {
    27. case i != -1 && j != -1:
    28. // Append a new renaming change.
    29. changes = append(changes, &atlas.RenameColumn{
    30. From: changes[i].(*atlas.DropColumn).C,
    31. To: changes[j].(*atlas.AddColumn).C,
    32. })
    33. // Remove the drop and add changes.
    34. changes.RemoveIndex(i, j)
    35. m.Changes = changes
    36. case i != -1 || j != -1:
    37. return nil, errors.New("old_name and new_name must be present or absent")
    38. }
    39. }
    40. return changes, nil
    41. })
    42. }

    Apply Hook Example

    The Apply hook allows accessing and mutating the migration plan and its raw changes (SQL statements), but in addition to that it is also useful for executing custom SQL statements before or after the plan is applied. For example, changing a nullable column to non-nullable without a default value is not allowed by default. However, we can work around this using an Apply hook that UPDATEs all rows that contain NULL value in this column:

    1. func main() {
    2. client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    3. if err != nil {
    4. log.Fatalf("failed connecting to mysql: %v", err)
    5. }
    6. defer client.Close()
    7. // ...
    8. if err := client.Schema.Create(ctx, schema.WithApplyHook(fillNulls)); err != nil {
    9. log.Fatalf("failed creating schema resources: %v", err)
    10. }
    11. }
    12. func fillNulls(next schema.Applier) schema.Applier {
    13. return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
    14. // There are three ways to UPDATE the NULL values to "Unknown" in this stage.
    15. // Append a custom migrate.Change to the plan, execute an SQL statement directly
    16. // on the dialect.ExecQuerier, or use the ent.Client used by the project.
    17. // Execute a custom SQL statement.
    18. query, args := sql.Dialect(dialect.MySQL).
    19. Update(user.Table).
    20. Set(user.FieldDropOptional, "Unknown").
    21. Where(sql.IsNull(user.FieldDropOptional)).
    22. Query()
    23. if err := conn.Exec(ctx, query, args, nil); err != nil {
    24. return err
    25. }
    26. // Append a custom statement to migrate.Plan.
    27. //
    28. // plan.Changes = append([]*migrate.Change{
    29. // {
    30. // Cmd: fmt.Sprintf("UPDATE users SET %[1]s = '%[2]s' WHERE %[1]s IS NULL", user.FieldDropOptional, "Unknown"),
    31. // },
    32. // }, plan.Changes...)
    33. // Use the ent.Client used by the project.
    34. //
    35. // drv := sql.NewDriver(dialect.MySQL, sql.Conn{ExecQuerier: conn.(*sql.Tx)})
    36. // if err := ent.NewClient(ent.Driver(drv)).
    37. // User.
    38. // Update().
    39. // SetDropOptional("Unknown").
    40. // Where(/* Add predicate to filter only rows with NULL values */).
    41. // Exec(ctx); err != nil {
    42. // return fmt.Errorf("fix default values to uppercase: %w", err)
    43. // }
    44. return next.Apply(ctx, conn, plan)