Saltar al contenido principal

Migraciones de datos

[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 →

Las migraciones se suelen utilizar para modificar el esquema de la base de datos, pero en ocasiones surge la necesidad de alterar los datos almacenados. Por ejemplo, para añadir datos iniciales (seed data) o rellenar columnas vacías con valores predeterminados personalizados.

Este tipo de migraciones se denominan migraciones de datos. En este documento exploraremos cómo utilizar Ent para planificar migraciones de datos e integrarlas en tu flujo habitual de migraciones de esquema.

Tipos de migración

Actualmente Ent admite dos tipos de migraciones: migraciones versionadas y migraciones declarativas (también conocidas como migración automática). Las migraciones de datos pueden ejecutarse en ambos tipos.

Migraciones versionadas

Al usar migraciones versionadas, las migraciones de datos deben almacenarse en el mismo directorio migrations y ejecutarse igual que las migraciones regulares. Sin embargo, se recomienda guardar migraciones de datos y de esquema en archivos separados para facilitar las pruebas.

El formato utilizado para estas migraciones es SQL, ya que el archivo puede ejecutarse con seguridad (y almacenarse sin cambios) incluso si el esquema de Ent se modifica y el código generado deja de ser compatible con el archivo de migración de datos.

Existen dos formas de crear scripts de migración de datos: manualmente o generados. Al editar manualmente, los usuarios escriben todas las sentencias SQL y controlan exactamente lo que se ejecutará. Alternativamente, pueden utilizar Ent para generar las migraciones de datos. Se recomienda verificar que el archivo generado es correcto, ya que en algunos casos podría necesitar correcciones o ediciones manuales.

Creación manual

1. Si no tienes Atlas instalado, consulta su guía de inicio.

2. Crea un nuevo archivo de migración usando Atlas:

atlas migrate new <migration_name> \
--dir "file://my/project/migrations"

3. Edita el archivo de migración y añade la migración de datos personalizada. Por ejemplo:

ent/migrate/migrations/20221126185750_backfill_data.sql
-- Backfill NULL or null tags with a default value.
UPDATE `users` SET `tags` = '["foo","bar"]' WHERE `tags` IS NULL OR JSON_CONTAINS(`tags`, 'null', '$');

4. Actualiza el archivo de integridad del directorio de migraciones:

atlas migrate hash \
--dir "file://my/project/migrations"

Consulta la sección Pruebas más abajo si no sabes cómo probar el archivo de migración de datos.

Scripts generados

Actualmente Ent ofrece soporte inicial para generar archivos de migración de datos. Esta opción simplifica el proceso de escribir manualmente sentencias SQL complejas en la mayoría de casos. Aun así, se recomienda verificar que el archivo generado es correcto, ya que en casos excepcionales podría necesitar edición manual.

1. Configura tu entorno de migraciones versionadas si aún no lo has hecho.

2. Crea tu primera función de migración de datos. A continuación encontrarás ejemplos que muestran cómo escribirla:

ent/migrate/migratedata/migratedata.go
package migratedata

// BackfillUnknown back-fills all empty users' names with the default value 'Unknown'.
func BackfillUnknown(dir *migrate.LocalDir) error {
w := &schema.DirWriter{Dir: dir}
client := ent.NewClient(ent.Driver(schema.NewWriteDriver(dialect.MySQL, w)))

// Change all empty names to 'unknown'.
err := client.User.
Update().
Where(
user.NameEQ(""),
).
SetName("Unknown").
Exec(context.Background())
if err != nil {
return fmt.Errorf("failed generating statement: %w", err)
}

// Write the content to the migration directory.
return w.FlushChange(
"unknown_names",
"Backfill all empty user names with default value 'unknown'.",
)
}

Then, using this function in ent/migrate/main.go will generate the following migration file:

migrations/20221126185750_unknown_names.sql
-- Backfill all empty user names with default value 'unknown'.
UPDATE `users` SET `name` = 'Unknown' WHERE `users`.`name` = '';

3. Si editas el archivo generado, actualiza el archivo de integridad del directorio de migraciones con este comando:

atlas migrate hash \
--dir "file://my/project/migrations"

Pruebas

Tras añadir los archivos de migración, es altamente recomendable aplicarlos en una base de datos local para verificar que son válidos y producen los resultados esperados. Este proceso puede hacerse manualmente o automatizarse con un programa.

1. Ejecuta todos los archivos de migración hasta el último creado (el archivo de migración de datos):

# Total number of files.
number_of_files=$(ls ent/migrate/migrations/*.sql | wc -l)

# Execute all files without the latest.
atlas migrate apply $[number_of_files-1] \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

2. Verifica que el último archivo de migración está pendiente de ejecución:

atlas migrate status \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

Migration Status: PENDING
-- Current Version: <VERSION_N-1>
-- Next Version: <VERSION_N>
-- Executed Files: <N-1>
-- Pending Files: 1

3. Rellena la base de datos local con datos temporales que representen la base de datos de producción antes de ejecutar el archivo de migración de datos.

4. Ejecuta atlas migrate apply y comprueba que se completó con éxito.

atlas migrate apply \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

Nota: usando atlas schema clean puedes limpiar la base de datos de desarrollo local y repetir este proceso hasta que el archivo de migración de datos produzca el resultado deseado.

Migraciones automáticas

En el flujo de trabajo declarativo, las migraciones de datos se implementan mediante Hooks de Diff o Apply. Esto se debe a que, a diferencia de la opción versionada, este tipo de migraciones no tienen nombre ni versión cuando se aplican. Por lo tanto, al escribir datos mediante hooks, es necesario verificar el tipo de schema.Change antes de su ejecución para garantizar que la migración de datos no se aplique más de una vez.

func FillNullValues(dbdialect string) schema.ApplyHook {
return func(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// Search the schema.Change that triggers the data migration.
hasC := func() bool {
for _, c := range plan.Changes {
m, ok := c.Source.(*schema.ModifyTable)
if ok && m.T.Name == user.Table && schema.Changes(m.Changes).IndexModifyColumn(user.FieldName) != -1 {
return true
}
}
return false
}()
// Change was found, apply the data migration.
if hasC {
// At this stage, there are three ways to UPDATE the NULL values to "Unknown".
// Append a custom migrate.Change to migrate.Plan, execute an SQL statement
// directly on the dialect.ExecQuerier, or use the generated ent.Client.

// Create a temporary client from the migration connection.
client := ent.NewClient(
ent.Driver(sql.NewDriver(dbdialect, sql.Conn{ExecQuerier: conn.(*sql.Tx)})),
)
if err := client.User.
Update().
SetName("Unknown").
Where(user.NameIsNil()).
Exec(ctx); err != nil {
return err
}
}
return next.Apply(ctx, conn, plan)
})
}
}

Para ver más ejemplos, consulta la sección de ejemplos de Apply Hook.