¡Hola, amigo!
Hoy, hablemos sobre qué son las migraciones de bases de datos y por qué son tan importantes. En el mundo de hoy, no es sorpresa que cualquier cambio en una base de datos deba hacerse con cuidado y siguiendo un proceso específico. Idealmente, estos pasos deberían integrarse en nuestra pipeline de CI/CD para que todo funcione automáticamente.
Aquí está nuestra agenda:
- ¿Cuál es el problema?
- ¿Cómo lo solucionamos?
- Un ejemplo simple
- Un ejemplo más complejo
- Recomendaciones
- Resultados
- Conclusión
¿Cuál es el problema?
Si tu equipo nunca ha tratado con migraciones de bases de datos y no estás del todo seguro de por qué son necesarias, aclaremos eso. Si ya conoces lo básico, siéntete libre de avanzar.
Desafío principal
Cuando hacemos cambios “planificados” y “suaves” en la base de datos, necesitamos mantener la disponibilidad del servicio y cumplir con los requisitos de SLA (para que los usuarios no sufran por tiempo de inactividad o retrasos). Imagina que quieres cambiar el tipo de una columna en una tabla con 5 millones de usuarios. Si haces esto “de frente” (por ejemplo, simplemente ejecutando ALTER TABLE
sin preparación), la tabla podría bloquearse por un tiempo significativo — y tus usuarios se quedarían sin servicio.
Para evitar tales dolores de cabeza, sigue dos reglas:
- Aplica migraciones de una manera que no bloquee la tabla (o al menos minimice los bloqueos).
- Si necesitas cambiar un tipo de columna, a menudo es más fácil crear primero una nueva columna con el tipo correcto y luego eliminar la antigua después.
Otro problema: Control de versiones y retrocesos
A veces necesitas revertir una migración.
Hacer esto manualmente —entrar en la base de datos de producción y manipular los datos— no solo es arriesgado, sino también probablemente imposible si no tienes acceso directo. Ahí es donde entran en juego las herramientas de migración dedicadas. Te permiten aplicar cambios de manera limpia y revertirlos si es necesario.
¿Cómo lo solucionamos? Usa las herramientas adecuadas
Cada lenguaje y ecosistema tiene sus propias herramientas de migración:
- Para Java, Liquibase o Flyway son comunes.
- Para Go, una opción popular es goose (la que veremos aquí).
- Y así sucesivamente.
Goose: Qué es y por qué es útil
Goose es una utilidad Go ligera que te ayuda a gestionar migraciones automáticamente. Ofrece:
- Simplicidad. Dependencias mínimas y una estructura de archivos transparente para las migraciones.
- Versatilidad. Admite varios controladores de bases de datos (PostgreSQL, MySQL, SQLite, etc.).
- Flexibilidad. Escribe migraciones en SQL o código Go.
Instalación de Goose
go install github.com/pressly/goose/v3/cmd/goose@latest
Cómo funciona: Estructura de migración
Por defecto, Goose busca archivos de migración en db/migrations
. Cada migración sigue este formato:
NNN_migration_name.(sql|go)
NNN
es el número de migración (por ejemplo,001
,002
, etc.).- Después de eso, puedes tener cualquier nombre descriptivo, por ejemplo
init_schema
. - La extensión puede ser
.sql
o.go
.
Ejemplo de una migración SQL
Archivo: 001_init_schema.sql
:
-- +goose Up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE users;
Nuestro primer ejemplo
Cambiando un tipo de columna (String → Int)
Supongamos que tenemos una tabla users
con una columna age
de tipo VARCHAR(255)
. Ahora queremos cambiarlo a INTEGER
. Así es como podría verse la migración (archivo 005_change_column_type.sql
):
-- +goose Up
ALTER TABLE users ALTER COLUMN age TYPE INTEGER USING (age::INTEGER);
-- +goose Down
ALTER TABLE users ALTER COLUMN age TYPE VARCHAR(255) USING (age::TEXT);
Lo que está sucediendo aquí:
-
Migración Up
- Cambiamos la columna
age
aINTEGER
. La cláusulaUSING (age::INTEGER)
le dice a PostgreSQL cómo convertir los datos existentes al nuevo tipo. - Nota que esta migración fallará si hay algún dato en
age
que no sea numérico. En ese caso, necesitarás una estrategia más compleja (ver abajo).
- Cambiamos la columna
-
Migración hacia abajo
- Si retrocedemos, devolvemos
age
aVARCHAR(255)
. - De nuevo usamos
USING (age::TEXT)
para convertir deINTEGER
a texto.
- Si retrocedemos, devolvemos
Los Casos Segundos y Complejos: Migraciones de Varios Pasos
Si la columna age
podría contener datos desordenados (no solo números), es más seguro hacerlo en varios pasos:
- Agregar una nueva columna (
age_int
) de tipoINTEGER
. - Copiar los datos válidos en la nueva columna, tratando o eliminando entradas inválidas.
- Eliminar la columna antigua.
-- +goose Up
-- Step 1: Add a new column
ALTER TABLE users ADD COLUMN age_int INTEGER;
-- Step 2: Try to move data over
UPDATE users
SET age_int = CASE
WHEN age ~ '^[0-9]+$' THEN age::INTEGER
ELSE NULL
END;
-- (optional) remove rows where data couldn’t be converted
-- DELETE FROM users WHERE age_int IS NULL;
-- Step 3: Drop the old column
ALTER TABLE users DROP COLUMN age;
-- +goose Down
-- Step 1: Recreate the old column
ALTER TABLE users ADD COLUMN age VARCHAR(255);
-- Step 2: Copy data back
UPDATE users
SET age = age_int::TEXT;
-- Step 3: Drop the new column
ALTER TABLE users DROP COLUMN age_int;
Para permitir un rollback adecuado, la sección Down
simplemente refleja las acciones al revés.
La Automatización es Clave
Para ahorrar tiempo, es realmente conveniente agregar comandos de migración a un Makefile (o cualquier otro sistema de construcción). A continuación se muestra un ejemplo de Makefile con los principales comandos Goose para PostgreSQL.
Supongamos:
- El DSN para la base de datos es
postgres://usuario:contraseña@localhost:5432/nombredb?sslmode=disable
. - Los archivos de migración están en
db/migrations
.
# File: Makefile
DB_DSN = "postgres://user:password@localhost:5432/dbname?sslmode=disable"
MIGRATIONS_DIR = db/migrations
# Install Goose (run once)
install-goose:
go install github.com/pressly/goose/v3/cmd/goose@latest
# Create a new SQL migration file
new-migration:
ifndef NAME
$(error Usage: make new-migration NAME=your_migration_name)
endif
goose -dir $(MIGRATIONS_DIR) create $(NAME) sql
# Apply all pending migrations
migrate-up:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) up
# Roll back the last migration
migrate-down:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) down
# Roll back all migrations (be careful in production!)
migrate-reset:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) reset
# Check migration status
migrate-status:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) status
¿Cómo se usa?
1. Crear una nueva migración (archivo SQL). Esto generará un archivo db/migrations/002_add_orders_table.sql
.
make new-migration NAME=add_orders_table
2. Aplicar todas las migraciones. Goose creará una tabla schema_migrations
en tu base de datos (si aún no existe) y aplicará cualquier nueva migración en orden ascendente.
make migrate-up
3. Deshacer la última migración. Solo deshacer la última.
make migrate-down
4. Deshacer todas las migraciones (usar con precaución en producción). Reinicio completo.
make migrate-reset
5. Verificar el estado de la migración.
make migrate-status
Ejemplo de salida:
$ goose status
$ Applied At Migration
$ =======================================
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
$ Pending -- 003_and_again.go
Resumen
Al utilizar herramientas de migración y un Makefile, podemos:
- Restringir el acceso directo a la base de datos de producción, realizando cambios solo a través de migraciones.
- Seguir fácilmente las versiones de la base de datos y revertirlas si algo sale mal.
- Mantener un historial único y consistente de cambios en la base de datos.
- Realizar migraciones “suaves” que no rompan un entorno de producción en ejecución en un mundo de microservicios.
- Obtener validación adicional: cada cambio pasará por un proceso de PR y revisión de código (suponiendo que tienes esa configuración en su lugar).
Otra ventaja es que es fácil integrar todos estos comandos en tu canal de CI/CD. Y recuerda: la seguridad por encima de todo.
Por ejemplo:
jobs
migrate
runs-on ubuntu-latest
steps
name Install Goose
run
make install-goose
name Run database migrations
env
DB_DSN $ secrets.DATABASE_URL
run
make migrate-up
Conclusión y Consejos
Las ideas principales son muy simples:
- Mantén tus migraciones pequeñas y frecuentes. Son más fáciles de revisar, probar y revertir si es necesario.
- Utiliza la misma herramienta en todos los entornos para que desarrollo, pruebas y producción estén sincronizados.
- Integra las migraciones en CI/CD para no depender de que una persona las ejecute manualmente.
De esta manera, tendrás un proceso confiable y controlado para cambiar la estructura de tu base de datos, uno que no rompa la producción y te permita responder rápidamente si algo sale mal.
¡Buena suerte con tus migraciones!
¡Gracias por leer!
Source:
https://dzone.com/articles/goose-as-crucial-tool-for-your-service