Saltar al contenido principal

Migración Automática

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

El soporte de migraciones en ent permite mantener el esquema de la base de datos alineado con los objetos definidos en ent/migrate/schema.go en la raíz de tu proyecto.

Migración Automática

Ejecuta la lógica de auto-migración durante la inicialización de la aplicación:

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

Create genera todos los recursos necesarios para tu proyecto ent. Por defecto, Create funciona en modo "solo anexar": crea nuevas tablas e índices, añade columnas a tablas existentes o extiende tipos de columnas (ej. cambiar int a bigint).

¿Y qué pasa con eliminar columnas o índices?

Eliminación de Recursos

WithDropIndex y WithDropColumn son dos opciones para eliminar índices y columnas de tablas.

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)
}
}

Para ejecutar la migración en modo depuración (mostrando todas las consultas SQL):

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

IDs Universales

Por defecto, las claves primarias SQL empiezan en 1 para cada tabla, permitiendo que entidades de distintos tipos compartan ID. A diferencia de AWS Neptune que usa UUIDs. Consulta esto para habilitar IDs únicos globales con SQL.

Modo Offline

Al convertirse Atlas pronto en el motor de migración predeterminado, este modo será reemplazado por migraciones versionadas.

El modo offline permite escribir cambios de esquema en un io.Writer antes de ejecutarlos. Es útil para:

  • Verificar comandos SQL antes de ejecutarlos
  • Generar scripts SQL para ejecución manual

Imprimir cambios

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)
}
}

Escribir cambios a archivo

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)
}
}

Claves Foráneas

Por defecto, ent usa claves foráneas en relaciones (edges) para garantizar consistencia directamente en la base de datos.

Sin embargo, ent también proporciona una opción para desactivar esta funcionalidad mediante WithForeignKeys. Ten en cuenta que al establecer esta opción en false:

  • No se crearán claves foráneas en el DDL del esquema
  • La validación y limpieza de relaciones debe gestionarse manualmente por el desarrollador

Próximamente ofreceremos hooks para implementar estas restricciones a nivel de aplicación.

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)
}
}

Hooks de Migración

El framework permite añadir hooks (middlewares) durante la migración. Son ideales para:

  • Modificar o filtrar tablas afectadas
  • Crear recursos personalizados en la base de datos
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)
}
}

Integración con Atlas

Desde v0.10, Ent soporta migraciones con Atlas, un framework más robusto que cubre funcionalidades no disponibles en el paquete actual. Usa WithAtlas(true) para activarlo.

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)
}
}

Además de opciones estándar (WithDropColumn, WithGlobalUniqueID), la integración con Atlas ofrece:

  • Opciones adicionales para intervenir en pasos de migración

atlas-migration-process

Hooks de Diff y Apply en Atlas

Aquí tenéis dos ejemplos que muestran cómo conectar con los pasos Diff y Apply de Atlas.

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)
}
}

Ejemplo de Hook Diff

Si un campo se renombra en ent/schema, Ent no detectará este cambio como renombrado y propondrá modificaciones DropColumn y AddColumn en la etapa de diferencias. Una solución es usar la opción StorageKey manteniendo el antiguo nombre de columna en la tabla. Sin embargo, los hooks Diff de Atlas permiten reemplazar los cambios DropColumn y AddColumn con una operación 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
})
}

Ejemplo de hook Apply

El hook Apply permite acceder y modificar el plan de migración y sus cambios brutos (sentencias SQL), además de ejecutar SQL personalizado antes/después de aplicar el plan. Por ejemplo, cambiar una columna nullable a non-nullable sin valor predeterminado no está permitido por defecto. Podemos solventarlo con un hook Apply que actualice (UPDATE) las filas con valores NULL en dicha columna:

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)
})
}