Migraciones de schema con Flyway

Avanzado
Spring Boot
Spring Boot
Actualizado: 07/05/2026

Diagrama: tutorial-spring-boot-flyway-migraciones

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:

  • update no 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:

  1. La primera vez que Flyway corre, crea flyway_schema_history y registra una entrada con versión 0.
  2. Las migraciones V001, V002... se aplicarán a partir de aquí.

Estrategia recomendada:

  1. Generar el schema actual con pg_dump o equivalente.
  2. Guardar como db/migration/V001__baseline_schema.sql (no se ejecutará gracias al baseline).
  3. Activar baseline-on-migrate: true y baseline-version: 1.
  4. 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):

  1. Expand: añadir la nueva forma sin tocar la antigua. La nueva versión usa ambas.
  2. Migrate data: copiar datos de la antigua a la nueva.
  3. Switch: la nueva versión usa solo la nueva forma.
  4. 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 B V200-V299.

Buenas prácticas

  • ddl-auto: validate (o none) 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:info en 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 - Autor del tutorial

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.