Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Cambiar el tipo de una columna en un esquema de base de datos puede parecer trivial a primera vista, pero en realidad es una operación arriesgada que puede causar problemas de compatibilidad entre el servidor y la base de datos. En esta publicación, exploraré cómo los desarrolladores pueden realizar este tipo de cambios sin causar tiempo de inactividad en su aplicación.
Recientemente, mientras trabajaba en una funcionalidad para Ariga Cloud, necesité cambiar el tipo de un campo de Ent de un blob no estructurado a un campo JSON estructurado. Cambiar el tipo de columna era necesario para poder utilizar JSON Predicates y realizar consultas más eficientes.
El esquema original se veía así en el diagrama de visualización de nuestro producto en la nube:

En nuestro caso, no podíamos simplemente copiar los datos directamente a la nueva columna, ya que los datos no son compatibles con el nuevo tipo de columna (los datos blob pueden no ser convertibles a JSON).
En el pasado, se consideraba aceptable detener el servidor, migrar el esquema de la base de datos a la siguiente versión, y solo entonces iniciar el servidor con la nueva versión compatible con el nuevo esquema de base de datos. Hoy en día, los requisitos empresariales suelen dictar que las aplicaciones deben ofrecer alta disponibilidad, lo que deja a los equipos de ingeniería el desafío de ejecutar cambios como este sin tiempo de inactividad.
El patrón común para satisfacer este tipo de requisito, como se define en "Diseño Evolutivo de Bases de Datos" de Martin Fowler, es utilizar una "fase de transición".
Una fase de transición es "un período de tiempo durante el cual la base de datos admite tanto el patrón de acceso antiguo como los nuevos simultáneamente. Esto permite que los sistemas antiguos migren a las nuevas estructuras a su propio ritmo", como ilustra este diagrama:
Crédito: martinfowler.com
Planeamos el cambio en 5 pasos simples, todos compatibles con versiones anteriores:
Crear una nueva columna llamada
meta_jsoncon tipo JSON.Implementar una versión de la aplicación que realice escrituras duales. Cada nuevo registro o actualización se escribe tanto en la nueva columna como en la antigua, mientras que las lecturas siguen ocurriendo desde la columna antigua.
Migrar los datos de la columna antigua a la nueva.
Implementar una versión de la aplicación que lea desde la nueva columna.
Eliminar la columna antigua.
Migraciones versionadas
En nuestro proyecto utilizamos el flujo de migraciones versionadas de Ent para gestionar el esquema de la base de datos. Las migraciones versionadas proporcionan a los equipos un control granular sobre cómo se realizan los cambios en el esquema de la base de datos de la aplicación. Este nivel de control será muy útil para implementar nuestro plan. Si tu proyecto usa Migraciones Automáticas y quieres seguir el proceso, primero actualiza tu proyecto para usar migraciones versionadas.
Lo mismo se puede hacer con migraciones automáticas utilizando la función Migraciones de Datos, sin embargo, esta publicación se centra en migraciones versionadas
Creando una columna JSON con Ent:
Primero, añadiremos un nuevo tipo JSON de Ent al esquema de usuario.
type Meta struct {
CreateTime time.Time `json:"create_time"`
UpdateTime time.Time `json:"update_time"`
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.Bytes("meta"),
field.JSON("meta_json", &types.Meta{}).Optional(),
}
}
Luego, ejecutamos la generación de código para actualizar el esquema de la aplicación:
go generate ./...
A continuación, ejecutamos nuestro script de planificación automática de migraciones que genera un conjunto de archivos de migración con las sentencias SQL necesarias para actualizar la base de datos a la versión más reciente.
go run -mod=mod ent/migrate/main.go add_json_meta_column
El archivo de migración resultante que describe el cambio:
-- modify "users" table
ALTER TABLE `users` ADD COLUMN `meta_json` json NULL;
Ahora aplicaremos el archivo de migración creado usando Atlas:
atlas migrate apply \
--dir "file://ent/migrate/migrations"
--url mysql://root:pass@localhost:3306/ent
Como resultado, obtenemos el siguiente esquema en nuestra base de datos:

Comenzar a escribir en ambas columnas
Tras generar el tipo JSON, comenzaremos a escribir en la nueva columna:
- err := client.User.Create().
- SetMeta(input.Meta).
- Exec(ctx)
+ var meta types.Meta
+ if err := json.Unmarshal(input.Meta, &meta); err != nil {
+ return nil, err
+ }
+ err := client.User.Create().
+ SetMetaJSON(&meta).
+ Exec(ctx)
Para garantizar que los valores escritos en la nueva columna meta_json se repliquen en la antigua, podemos utilizar la función Schema Hooks de Ent. Esto requiere añadir una importación en blanco ent/runtime en tu main para registrar el hook y evitar importaciones circulares:
// Hooks of the User.
func (User) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) {
meta, ok := m.MetaJSON()
if !ok {
return next.Mutate(ctx, m)
}
if b, err := json.Marshal(meta); err != nil {
return nil, err
}
m.SetMeta(b)
return next.Mutate(ctx, m)
})
},
ent.OpCreate,
),
}
}
Tras asegurar las escrituras en ambos campos, podemos desplegar en producción con seguridad.
Rellenar valores desde la columna antigua
Ahora en nuestra base de datos de producción tenemos dos columnas: una almacena el objeto meta como blob y otra como JSON. La segunda columna puede tener valores nulos ya que se añadió recientemente, por lo que debemos rellenarla con los valores de la columna antigua.
Para ello, creamos manualmente un archivo de migración SQL que rellenará la nueva columna JSON con los datos de la antigua columna blob.
También puedes escribir código Go que genere este archivo de migración de datos usando el WriteDriver.
Crea un nuevo archivo de migración vacío:
atlas migrate new --dir file://ent/migrate/migrations
Para cada fila en la tabla de usuarios con valor JSON nulo (filas añadidas antes de crear la nueva columna), intentamos analizar el objeto meta como JSON válido. Si tiene éxito, rellenamos meta_json con el valor resultante; de lo contrario, lo marcamos como vacío.
Nuestro siguiente paso es editar el archivo de migración:
UPDATE users
SET meta_json = CASE
-- when meta is valid json stores it as is.
WHEN JSON_VALID(cast(meta as char)) = 1 THEN cast(cast(meta as char) as json)
-- if meta is not valid json, store it as an empty object.
ELSE JSON_SET('{}')
END
WHERE meta_json is null;
Recalcula el hash del directorio de migraciones tras modificar un archivo:
atlas migrate hash --dir "file://ent/mirate/migrations"
Podemos probar el archivo de migración ejecutando todas las migraciones anteriores en una base de datos local, poblándola con datos temporales y aplicando la última migración para verificar su funcionamiento.
Tras las pruebas, aplicamos el archivo de migración:
atlas migrate apply \
--dir "file://ent/migrate/migrations"
--url mysql://root:pass@localhost:3306/ent
Ahora realizaremos otro despliegue en producción.
Redirigir lecturas a la nueva columna y eliminar la antigua
Al tener valores en meta_json, podemos cambiar las lecturas del campo antiguo al nuevo.
En lugar de decodificar los datos desde user.meta en cada lectura, usa directamente el campo meta_json:
- var meta types.Meta
- if err = json.Unmarshal(user.Meta, &meta); err != nil {
- return nil, err
- }
- if meta.CreateTime.Before(time.Unix(0, 0)) {
- return nil, errors.New("invalid create time")
- }
+ if user.MetaJSON.CreateTime.Before(time.Unix(0, 0)) {
+ return nil, errors.New("invalid create time")
+ }
Tras redirigir las lecturas, desplegamos los cambios en producción.
Eliminar la columna antigua
Ahora podemos eliminar el campo de la columna antigua del esquema Ent, pues ya no lo usamos.
func (User) Fields() []ent.Field {
return []ent.Field{
- field.Bytes("meta"),
field.JSON("meta_json", &types.Meta{}).Optional(),
}
}
Genera nuevamente el esquema Ent con la función Drop Column activada.
go run -mod=mod ent/migrate/main.go drop_user_meta_column
Tras crear correctamente el nuevo campo, redirigir escrituras, rellenar datos y eliminar la columna antigua, estamos listos para el despliegue final. ¡Solo queda fusionar nuestro código en control de versiones y desplegar en producción!
Para finalizar
En esta publicación hemos explicado cómo cambiar el tipo de una columna en producción sin tiempo de inactividad usando migraciones versionadas de Atlas integradas con Ent.
¿Tienes preguntas? ¿Necesitas ayuda para comenzar? Únete a nuestro Servidor de Discord de Ent.
- Suscríbete a nuestro Newsletter
- Síguenos en Twitter
- Únete a #ent en Gophers Slack
- Únete al Servidor de Discord de Ent