Flyway

Setup

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<!-- Flyway 10+: database support is a separate module -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>              <!-- or flyway-database-postgresql -->
</dependency>

Configuration

application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: validate
  flyway:
    enabled: true                   # default when flyway-core is on the classpath
    locations: classpath:db/migration
    baseline-on-migrate: false      # true only when adopting Flyway on an existing database

Writing Migrations

File names follow V<version>__<description>.sql (two underscores). Versions are compared as numbers: V1_1 (1.1) runs before V2, and V10 after V9.

-- V1__create_categories_and_products.sql
CREATE TABLE categories (
    id   BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE
);
CREATE SEQUENCE product_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE products (
    id          BIGINT PRIMARY KEY,
    name        VARCHAR(200) NOT NULL,
    description TEXT,
    price       NUMERIC(10,2) NOT NULL,
    stock       INT NOT NULL DEFAULT 0,
    version     BIGINT NOT NULL DEFAULT 0,
    category_id BIGINT NOT NULL REFERENCES categories(id)
);
CREATE INDEX idx_products_category ON products(category_id);

-- V3__add_product_sku.sql
ALTER TABLE products ADD COLUMN sku VARCHAR(40);
UPDATE products SET sku = CONCAT('SE-', id) WHERE sku IS NULL;
ALTER TABLE products ALTER COLUMN sku SET NOT NULL;
ALTER TABLE products ADD CONSTRAINT uk_products_sku UNIQUE (sku);

How Flyway Decides What to Run

Example

A mini Flyway: sort by numeric version, skip what the history table already has, and refuse to start if an applied script was edited (checksum mismatch):

import java.util.*;
import java.util.zip.CRC32;

public class Main {
    record Applied(String version, long checksum) {}
    static final Map<String, Applied> history = new LinkedHashMap<>();   // flyway_schema_history

    static long checksum(String sql) { CRC32 c = new CRC32(); c.update(sql.getBytes()); return c.getValue(); }

    static int[] parse(String v) { return Arrays.stream(v.split("\\.")).mapToInt(Integer::parseInt).toArray(); }

    static void migrate(Map<String, String> scripts) {
        List<String> versions = new ArrayList<>(scripts.keySet());
        versions.sort((a, b) -> Arrays.compare(parse(a), parse(b)));       // numeric, not alphabetical
        for (String v : versions) {
            long sum = checksum(scripts.get(v));
            Applied done = history.get(v);
            if (done == null) {
                history.put(v, new Applied(v, sum));
                System.out.println("  applied V" + v);
            } else if (done.checksum() != sum) {
                System.out.println("  ERROR: Validate failed: checksum mismatch for V" + v);
                return;
            }
        }
        System.out.println("  schema is at version " + versions.get(versions.size() - 1));
    }

    public static void main(String[] args) {
        Map<String, String> scripts = new HashMap<>(Map.of(
            "1", "create table products ...", "2", "create table orders ...", "10", "add index ...", "1.1", "add column stock"));
        System.out.println("deploy 1:"); migrate(scripts);
        scripts.put("11", "add column sku");
        System.out.println("deploy 2:"); migrate(scripts);
        scripts.put("2", "create table orders ... -- edited after release!");
        System.out.println("deploy 3:"); migrate(scripts);
    }
}

Output

deploy 1:
  applied V1
  applied V1.1
  applied V2
  applied V10
  schema is at version 10
deploy 2:
  applied V11
  schema is at version 11
deploy 3:
  ERROR: Validate failed: checksum mismatch for V2

Rules

  • Never edit a migration that has run anywhere shared. Write a new one.
  • One logical change per file; keep files small and reviewable.
  • R__name.sql = repeatable migration, re-run whenever its checksum changes (views, functions).
  • Java-based migrations (class V5__Backfill implements JavaMigration) for complex data changes.
  • Never run flyway clean against production (disable it: spring.flyway.clean-disabled=true, the default in recent versions).