
Por qué versionar el schema
En desarrollo intermedio, lo habitual es dejar que Hibernate cree y modifique el schema con spring.jpa.hibernate.ddl-auto=update. Eso funciona en local pero en producción es inaceptable:
updateno maneja eliminaciones de columnas, renombrados ni cambios de tipo.- No hay forma de saber en qué estado está el schema en cada entorno.
- Imposible coordinar con CI/CD o con despliegues blue/green.
- Imposible hacer rollback a una versión anterior.
Flyway versiona el schema como código: cada cambio es un archivo SQL numerado, aplicado en orden, registrado en una tabla. Pasa lo mismo entre dev, staging y prod, y se integra con el ciclo de despliegue.
Configuración mínima
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
spring:
jpa:
hibernate:
ddl-auto: validate # Hibernate solo verifica que el schema coincide con las @Entity
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: false
table: flyway_schema_history
Con esto, Spring Boot ejecuta Flyway al arrancar antes que Hibernate. Si Hibernate detecta una diferencia entre las entidades y el schema, falla en arranque (lo que es deseable: prefieres fallar al arrancar que servir tráfico con un schema desfasado).
Estructura de migraciones
En src/main/resources/db/migration/:
V001__create_empleado.sql
V002__create_departamento.sql
V003__add_email_unique_to_empleado.sql
V004__add_salario_check.sql
R__refresh_view_empleados_dashboard.sql
Versioned migrations (V)
Se ejecutan una vez y solo si su versión es nueva. El nombre es V<version>__<descripción>.sql. La versión puede ser semver (V001, V1.0.0, V20260507.143000) y se ordena lexicográficamente.
-- V001__create_empleado.sql
CREATE TABLE empleado (
id BIGSERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
salario NUMERIC(10, 2) NOT NULL,
departamento_id BIGINT REFERENCES departamento(id),
fecha_alta TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
fecha_baja TIMESTAMP
);
CREATE INDEX idx_empleado_departamento ON empleado(departamento_id);
CREATE INDEX idx_empleado_fecha_alta ON empleado(fecha_alta DESC);
Repeatable migrations (R)
Se ejecutan cada vez que su contenido cambia. Útiles para vistas, procedimientos almacenados, funciones y triggers, donde la definición se reemplaza completa:
-- R__refresh_view_empleados_dashboard.sql
CREATE OR REPLACE VIEW empleados_dashboard AS
SELECT
d.nombre AS departamento,
COUNT(*) AS total_empleados,
AVG(e.salario) AS salario_medio,
MAX(e.fecha_alta) AS ultima_alta
FROM empleado e
JOIN departamento d ON d.id = e.departamento_id
WHERE e.fecha_baja IS NULL
GROUP BY d.nombre;
Si modificas la vista, Flyway detecta el cambio del checksum del archivo y reaplica. Con versioned tendrías que crear una migración nueva cada vez. Con repeatable solo modificas el archivo.
Tabla flyway_schema_history
Flyway registra cada migración aplicada en una tabla:
SELECT installed_rank, version, description, type, script,
checksum, installed_on, execution_time, success
FROM flyway_schema_history;
installed_rank | version | description | type | script | checksum | installed_on | execution_time | success
----------------+---------+---------------------+------+----------------------------------+-----------+-----------------------------+----------------+---------
1 | 001 | create empleado | SQL | V001__create_empleado.sql | 234567890 | 2026-01-15 09:00:00 | 45 | t
2 | 002 | create departamento | SQL | V002__create_departamento.sql | 345678901 | 2026-01-15 09:00:01 | 32 | t
3 | 003 | add email unique | SQL | V003__add_email_unique.sql | 456789012 | 2026-02-20 14:30:00 | 18 | t
Esta tabla es la fuente de verdad del estado del schema. En staging o prod, basta una consulta para saber exactamente qué versiones están aplicadas.
Adoptar Flyway en un proyecto legacy
Si la BD ya existe (proyecto en curso), Flyway no puede aplicar la V001 porque las tablas ya están. Hay que establecer una baseline:
spring:
flyway:
baseline-on-migrate: true
baseline-version: 0
baseline-description: "Initial baseline of legacy schema"
Con esto:
- La primera vez que Flyway corre, crea
flyway_schema_historyy registra una entrada con versión 0. - Las migraciones
V001,V002... se aplicarán a partir de aquí.
Estrategia recomendada:
- Generar el schema actual con
pg_dumpo equivalente. - Guardar como
db/migration/V001__baseline_schema.sql(no se ejecutará gracias al baseline). - Activar
baseline-on-migrate: trueybaseline-version: 1. - Las nuevas migraciones empiezan en
V002.
Flyway placeholders
Flyway sustituye placeholders al ejecutar el SQL, útil para diferenciar entornos:
spring:
flyway:
placeholders:
schema_name: empleados_db
seed_data_enabled: ${app.flyway.seed-enabled:false}
-- V010__seed_test_data.sql
${seed_data_enabled:false} -- comentario condicional, no afecta
INSERT INTO empleado (nombre, email, salario)
VALUES ('Test User', 'test@example.com', 0);
En realidad los placeholders son más útiles para schema names en multi-tenant, no para condicionales (para eso, mejor migración separada por perfil).
Callbacks
Flyway permite ejecutar SQL antes/después de eventos:
db/migration/
db/callback/
beforeMigrate.sql -- antes de la primera migración pendiente
afterMigrate.sql -- después de aplicar todas las pendientes
beforeEach.sql -- antes de cada migración
-- db/callback/afterMigrate.sql
ANALYZE empleado;
ANALYZE departamento;
Útil para refrescar estadísticas, limpiar caches o disparar notificaciones. En general no se abusan: los callbacks ocultan side effects que pueden sorprender.
Integración con CI/CD
En arranque de la aplicación
Lo más común. Flyway corre antes de Spring inicializar JPA. Si una migración falla, la aplicación no arranca. Adecuado para microservicios donde cada deploy actualiza su schema.
Riesgo: si la migración es lenta (añadir índice a tabla de 100 millones de filas), bloquea el arranque y puede generar timeouts en orquestadores como Kubernetes.
Con maven-flyway-plugin (separado del arranque)
Para migraciones críticas que se ejecutan antes del despliegue:
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>10.x</version>
<configuration>
<url>${env.DB_URL}</url>
<user>${env.DB_USER}</user>
<password>${env.DB_PASSWORD}</password>
</configuration>
</plugin>
# Pipeline CI antes del deploy
mvn flyway:migrate
# Si falla, abortar deploy
# Si OK, continuar despliegue
Spring Boot al arrancar valida con validate-on-migrate: true que el schema coincide pero no aplica nada (porque ya se aplicó en el step anterior).
Despliegues blue/green con expand-contract
El patrón expand-contract permite migrar sin downtime cuando hay cambios incompatibles (renombrar columna, eliminar tabla):
- Expand: añadir la nueva forma sin tocar la antigua. La nueva versión usa ambas.
- Migrate data: copiar datos de la antigua a la nueva.
- Switch: la nueva versión usa solo la nueva forma.
- Contract: una vez confirmado, migrar para eliminar la antigua.
Ejemplo: renombrar nombre a nombre_completo:
-- V010__add_nombre_completo.sql
ALTER TABLE empleado ADD COLUMN nombre_completo VARCHAR(100);
UPDATE empleado SET nombre_completo = nombre;
ALTER TABLE empleado ALTER COLUMN nombre_completo SET NOT NULL;
Despliegue versión N+1 que escribe en ambas columnas y lee de nombre_completo. Confirmado funcional, después:
-- V020__remove_nombre.sql
ALTER TABLE empleado DROP COLUMN nombre;
Despliegue versión N+2 que solo usa nombre_completo. La columna antigua se elimina cuando todas las réplicas usan ya la nueva.
Tests con Testcontainers
Lo idiomático: probar las migraciones contra el motor real (PostgreSQL, MySQL) usando Testcontainers:
@SpringBootTest
@Testcontainers
class FlywayMigrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18-alpine");
@DynamicPropertySource
static void config(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void todasLasMigracionesSeAplican(@Autowired Flyway flyway) {
var migrations = flyway.info().applied();
assertThat(migrations).isNotEmpty();
assertThat(migrations[migrations.length - 1].getVersion().getVersion())
.isEqualTo("003");
}
@Test
void schemaCumpleConEntidades(@Autowired DataSource dataSource) throws SQLException {
try (var conn = dataSource.getConnection();
var stmt = conn.createStatement();
var rs = stmt.executeQuery("SELECT * FROM empleado WHERE 1=0")) {
var meta = rs.getMetaData();
assertThat(meta.getColumnCount()).isEqualTo(7);
}
}
}
Esto detecta migraciones rotas, datos de seed inválidos y discrepancias con las entidades antes de llegar a producción.
Errores comunes
Editar una migración aplicada
Cambiar el SQL de un archivo V001__... después de que se haya ejecutado rompe el checksum. Flyway detecta la diferencia y aborta. Es comportamiento correcto: si necesitas cambiar algo, crea una migración nueva (V005__fix_xxx.sql).
Para entornos de desarrollo donde no importa, se puede usar flyway:repair que actualiza el checksum sin reaplicar. Pero nunca en producción.
Usar UPDATE en migraciones grandes
Una migración con UPDATE empleado SET email_lower = LOWER(email) sobre 100 millones de filas puede tardar horas, bloquear la tabla y consumir todo el WAL.
Estrategias:
- Migrar en lotes con un script externo que se ejecuta separado del deploy.
- Usar columnas generadas (
GENERATED ALWAYS AS) en lugar de UPDATE batch. - Hacer la migración en background con un job, controlando el progreso.
Flyway versioned no es la herramienta correcta para data migrations masivas. Es para schema, no para data.
Ignorar el orden lexicográfico
V10__... se aplica antes que V2__... lexicográficamente. Por eso lo idiomático es usar 3 dígitos (V001, V010, V100) o timestamps (V20260507_143000).
Mezclar migraciones de equipos en main
En proyectos con varios equipos, dos PRs pueden añadir V005__... simultáneamente. Cuando se mergean, Flyway falla porque hay dos versiones iguales. Soluciones:
- Numerar por timestamp:
V20260507_143000__...reduce colisiones drásticamente. - Reservar rangos por equipo: equipo A usa
V100-V199, equipo BV200-V299.
Buenas prácticas
ddl-auto: validate(onone) en producción. Hibernate no toca el schema.- Versionar el SQL en el repo junto al código. Toda migración pasa por code review.
- Una migración por cambio lógico. No mezclar varios cambios independientes en una.
- Migraciones idempotentes cuando sea posible (
CREATE TABLE IF NOT EXISTS,ALTER TABLE ... IF NOT EXISTS). Reduce sorpresas en re-ejecuciones. - Tests con Testcontainers del motor real, no H2.
- Expand-contract para cambios incompatibles. Nunca renombrar/eliminar columnas en una sola migración cuando la app está corriendo.
flyway:infoen CI para mostrar qué migraciones se aplicarían antes del deploy.- No editar migraciones aplicadas. Siempre añadir nueva migración para corregir.
Flyway no resuelve la complejidad de las migraciones grandes; la estructura. Versionar el schema, automatizar su aplicación y tener trazabilidad completa de cada cambio convierte una aplicación frágil en un sistema operable. Es una de las inversiones de mayor retorno por mínimo coste en proyectos Spring Boot.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Boot
Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Versionar el schema de la base de datos con Flyway. Diferenciar migraciones versioned (V) y repeatable (R). Adoptar Flyway en un proyecto legacy con baseline. Coordinar migraciones con despliegues blue/green. Probar migraciones contra una BD real con Testcontainers.