Ir al contenido

Flyway Cheat Sheet

Overview

Flyway is a database migration tool that emphasizes simplicity and convention over configuration. It uses versioned SQL scripts (or Java-based migrations) to evolve database schemas. Flyway tracks which migrations have been applied using a flyway_schema_history table and applies pending migrations in order. It supports over 20 databases including PostgreSQL, MySQL, Oracle, SQL Server, MariaDB, and SQLite.

Flyway follows a straightforward approach: place numbered SQL files in a migrations directory, and Flyway figures out which ones need to run. It supports versioned migrations (V), repeatable migrations (R), and undo migrations (U, Teams/Enterprise). Flyway is available as a CLI tool, Maven/Gradle plugin, Java API, and Docker image.

Installation

# macOS via Homebrew
brew install flyway

# Linux
curl -L https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.15.0/flyway-commandline-10.15.0-linux-x64.tar.gz | tar xz
sudo ln -s $(pwd)/flyway-10.15.0/flyway /usr/local/bin/flyway

# Docker
docker pull flyway/flyway

# Verify installation
flyway --version

Build Tool Integration

<!-- Maven pom.xml -->
<plugin>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-maven-plugin</artifactId>
  <version>10.15.0</version>
  <configuration>
    <url>jdbc:postgresql://localhost:5432/mydb</url>
    <user>postgres</user>
    <password>password</password>
  </configuration>
</plugin>
// Gradle build.gradle
plugins {
    id 'org.flywaydb.flyway' version '10.15.0'
}

flyway {
    url = 'jdbc:postgresql://localhost:5432/mydb'
    user = 'postgres'
    password = 'password'
}

Core Commands

CommandDescription
flyway migrateApply pending migrations
flyway infoShow migration status and history
flyway validateValidate applied migrations match local files
flyway baselineBaseline an existing database
flyway repairRepair the schema history table
flyway cleanDrop all objects in the schema (dangerous)
flyway undoUndo the last migration (Teams/Enterprise)
# Apply pending migrations
flyway -url=jdbc:postgresql://localhost:5432/mydb \
  -user=postgres -password=password migrate

# Show migration status
flyway -url=jdbc:postgresql://localhost:5432/mydb \
  -user=postgres -password=password info

# Validate migrations
flyway -url=jdbc:postgresql://localhost:5432/mydb \
  -user=postgres -password=password validate

# Baseline at version 1 (for existing databases)
flyway -url=jdbc:postgresql://localhost:5432/mydb \
  -user=postgres -password=password -baselineVersion=1 baseline

# Dry run (preview SQL without executing)
flyway -url=jdbc:postgresql://localhost:5432/mydb \
  -user=postgres -password=password -dryRunOutput=dryrun.sql migrate

Migration File Naming

Naming Convention

V{version}__{description}.sql     — Versioned migration
R__{description}.sql              — Repeatable migration
U{version}__{description}.sql     — Undo migration (Teams/Enterprise)

Examples:
V1__create_users_table.sql
V1.1__add_email_to_users.sql
V2__create_posts_table.sql
V2.1.3__add_index_on_posts.sql
R__create_views.sql
U2__undo_create_posts_table.sql

Versioned Migrations

-- V1__create_users_table.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
-- V2__create_posts_table.sql
CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    content TEXT,
    status VARCHAR(20) DEFAULT 'draft',
    user_id INTEGER NOT NULL REFERENCES users(id),
    published_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
-- V3__add_tags_system.sql
CREATE TABLE tags (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE post_tags (
    post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
    PRIMARY KEY (post_id, tag_id)
);

Repeatable Migrations

-- R__create_views.sql
-- Repeatable migrations run whenever their checksum changes

CREATE OR REPLACE VIEW active_users AS
SELECT id, email, name, created_at
FROM users
WHERE last_login > CURRENT_TIMESTAMP - INTERVAL '30 days';

CREATE OR REPLACE VIEW published_posts AS
SELECT p.id, p.title, p.content, u.name as author, p.published_at
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'published'
ORDER BY p.published_at DESC;

Configuration

flyway.conf

# flyway.conf (or flyway.toml)
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=postgres
flyway.password=password
flyway.schemas=public
flyway.locations=filesystem:sql/migrations
flyway.baselineOnMigrate=true
flyway.baselineVersion=0
flyway.validateOnMigrate=true
flyway.outOfOrder=false
flyway.cleanDisabled=true
flyway.table=flyway_schema_history
flyway.placeholders.env=production
flyway.encoding=UTF-8
flyway.connectRetries=3
flyway.connectRetriesInterval=5

TOML Configuration

# flyway.toml
[flyway]
url = "jdbc:postgresql://localhost:5432/mydb"
user = "postgres"
password = "password"
locations = ["filesystem:sql/migrations"]
baselineOnMigrate = true
cleanDisabled = true
validateOnMigrate = true

[flyway.placeholders]
env = "production"
schema = "public"

Environment-Specific Configuration

# flyway-dev.conf
flyway.url=jdbc:postgresql://localhost:5432/mydb_dev
flyway.user=dev_user
flyway.password=dev_password
flyway.cleanDisabled=false

# flyway-prod.conf
flyway.url=jdbc:postgresql://prod-host:5432/mydb
flyway.user=prod_user
flyway.password=prod_password
flyway.cleanDisabled=true
# Use specific config file
flyway -configFiles=flyway-dev.conf migrate

Placeholders

-- V4__create_schema.sql
-- Use placeholders for environment-specific values
CREATE SCHEMA IF NOT EXISTS ${schema_name};
CREATE TABLE ${schema_name}.config (
    key VARCHAR(100) PRIMARY KEY,
    value TEXT,
    environment VARCHAR(20) DEFAULT '${env}'
);
# flyway.conf
flyway.placeholders.schema_name=app
flyway.placeholders.env=production

Advanced Usage

Java-Based Migrations

// V5__populate_seed_data.java
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Statement;

public class V5__populate_seed_data extends BaseJavaMigration {
    @Override
    public void migrate(Context context) throws Exception {
        try (Statement stmt = context.getConnection().createStatement()) {
            stmt.execute("INSERT INTO users (email, name, password_hash) "
                + "VALUES ('admin@example.com', 'Admin', 'hashed_password')");
        }
    }
}

Callbacks

-- beforeMigrate.sql — runs before each migrate command
SELECT 'Starting migration at ' || CURRENT_TIMESTAMP;

-- afterMigrate.sql — runs after each migrate command
REFRESH MATERIALIZED VIEW IF EXISTS user_stats;

-- beforeEachMigrate.sql — runs before each individual migration
-- afterEachMigrate.sql — runs after each individual migration

Docker Compose

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"

  flyway:
    image: flyway/flyway
    depends_on:
      - db
    volumes:
      - ./sql/migrations:/flyway/sql
      - ./flyway.conf:/flyway/conf/flyway.conf
    command: migrate

CI/CD Integration

# GitHub Actions
name: Database Migration
on:
  push:
    branches: [main]
    paths: ['sql/migrations/**']
jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Flyway
        run: |
          docker run --rm \
            -v $(pwd)/sql/migrations:/flyway/sql \
            flyway/flyway \
            -url="${{ secrets.DB_URL }}" \
            -user="${{ secrets.DB_USER }}" \
            -password="${{ secrets.DB_PASS }}" \
            migrate

Spring Boot Integration

# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.clean-disabled=true
spring.flyway.validate-on-migrate=true
spring.flyway.out-of-order=false

Troubleshooting

IssueSolution
Migration checksum mismatchNever modify applied migrations; use flyway repair to fix history table
Out of order errorSet outOfOrder=true or apply migrations sequentially
Baseline needed for existing DBRun flyway baseline before first migrate or set baselineOnMigrate=true
Driver not foundAdd JDBC driver to flyway/drivers/ directory
Schema history table missingRun flyway baseline or let migrate create it automatically
Clean accidentally runRestore from backup; set cleanDisabled=true in production configs
Migration failed mid-wayFix the SQL, run flyway repair to remove failed entry, then migrate again
Placeholder not resolvedCheck placeholder syntax ${name} and ensure it is defined in config
Multiple schemasSet flyway.schemas to comma-separated list; first schema is the default