RizTech Academy logo
RizTech Academy
Data Access with JPALesson 6 of 630 min

Schema migrations with Flyway

Your entities describe the schema you want; something has to make the real database match, and keep it matching as the schema changes over time. In development, Hibernate can create tables from your entities (ddl-auto), but in production that is dangerous. The professional answer is a migration tool — Flyway — that versions your schema as a series of SQL scripts, applied in order, tracked in the database. This lesson is why ddl-auto is not enough and how Flyway replaces it.

Why not let Hibernate manage the schema?

Spring Boot's spring.jpa.hibernate.ddl-auto can create or update tables from your entities:

  • create / create-drop — drop and recreate the schema on startup. Wipes all data. Development throwaway only.
  • update — try to alter the schema to match the entities. Convenient in early development, but it never drops or renames safely, applies changes unpredictably, and has no record of what changed or when. Never trust it in production.
  • validate — do not change anything; just check the schema matches the entities and fail startup if not. This is the production-safe setting.
  • none — do nothing.

The problem with update in production is stark: you have no versioned history of schema changes, no way to review a change before it runs, no way to reproduce the exact schema on another environment, and no safe handling of destructive changes. Letting Hibernate guess at ALTER TABLE on a database holding real data is how you lose data. So: ddl-auto=update for quick local prototyping at most; validate in production, with the actual schema managed by migrations.

Flyway: versioned SQL migrations

Flyway manages the schema as an ordered series of SQL scripts. Add flyway-core (Spring Boot auto-configures it) and put migration files in src/main/resources/db/migration, named by convention:

src/main/resources/db/migration/
  V1__create_hub_and_parcel.sql
  V2__add_parcel_status_index.sql
  V3__add_rider_table.sql

The naming is strict and meaningful: V + a version number + __ (two underscores) + a description. Flyway applies them in version order, once each. A first migration for Dakiya:

-- V1__create_hub_and_parcel.sql
CREATE TABLE hub (
    id   BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    city VARCHAR(60)  NOT NULL
);

CREATE TABLE parcel (
    id               BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    tracking_code    VARCHAR(20) NOT NULL UNIQUE,
    recipient_name   VARCHAR(100) NOT NULL,
    destination_city VARCHAR(60) NOT NULL,
    status           VARCHAR(20) NOT NULL,
    hub_id           BIGINT REFERENCES hub(id)
);

On startup, Flyway checks which migrations have run and applies any new ones, in order. You write the SQL yourself, so you control exactly what happens to the schema — including safe, deliberate handling of renames and data migrations that ddl-auto could never do.

How Flyway tracks what has run

Flyway keeps a table — flyway_schema_history — recording every migration applied, its version, checksum, and when it ran. This is the mechanism behind the guarantees:

  • Each migration runs once. Flyway compares the files against the history table and applies only the new ones. Running the app again applies nothing new — it is idempotent and safe on every deploy.
  • Migrations are immutable once applied. Flyway stores a checksum of each file; if you edit a migration that has already run, the checksum no longer matches and Flyway fails at startup. This is deliberate and correct: an applied migration is history, and history does not change. To change the schema, you add a new migration (V4__...), never edit an old one.
  • Every environment converges. Dev, staging and production all run the same ordered scripts, so they reach the identical schema — the same reproducibility Django's migrations give.

This is why Flyway (or Liquibase, the main alternative) is standard for production Spring: your schema has a version-controlled, reviewable, reproducible history, and flyway_schema_history is the single source of truth for what state a database is in.

Flyway and JPA together: the sane setup

The two work together with a clear division of labour, and this is the production configuration:

spring.jpa.hibernate.ddl-auto=validate     # Hibernate does NOT change the schema — Flyway owns it
spring.flyway.enabled=true                 # Flyway applies the migrations
  • Flyway owns the schema — it creates and alters tables via the migration scripts.
  • Hibernate validates — ddl-auto=validate checks your entities match the Flyway-created schema and fails fast at startup if they have drifted (an entity field with no column, say). This catches the mistake where you change an entity but forget to write the migration.

So the workflow for a schema change becomes: change the entity, and write a new V#__... migration for it; on startup Flyway applies the migration and Hibernate validates the entity against it. If you forget the migration, validate fails loudly — exactly the safety you want. The rules to carry: never ddl-auto anything but validate in production, own the schema with versioned Flyway migrations, never edit an applied migration (add a new one), and let Hibernate validate that entities and schema agree.

Check your work

Why not ddl-auto in production. create/create-drop wipe data; update changes the schema unpredictably with no history, review, or safe destructive changes. validate (check only) is the production-safe setting; update is for local prototyping at most.

Flyway. Versioned SQL scripts in db/migration, named V<version>__<description>.sql, applied in order, once each. You write the SQL, controlling exactly what happens.

flyway_schema_history. Records every applied migration with a checksum: each runs once (idempotent deploys), applied migrations are immutable (editing one fails the checksum — add a new migration instead), and every environment converges to the same schema.

Flyway + JPA setup. ddl-auto=validate + spring.flyway.enabled=true: Flyway owns/changes the schema, Hibernate validates that entities match and fails fast if you changed an entity without a migration.

Practice

  1. Set ddl-auto=update, add a field to an entity, and watch Hibernate alter the table — then reason about why you would not trust that in production (no history, no review).
  2. Add flyway-core, write V1__...sql creating your tables, set ddl-auto=validate, and confirm Flyway applies it on startup and the app runs.
  3. Inspect the flyway_schema_history table and see the recorded migration (version, checksum, timestamp).
  4. Edit the already-applied V1 file, restart, and observe Flyway fail on the checksum mismatch; revert and add a V2 instead.
  5. Change an entity (add a field) without a matching migration and confirm ddl-auto=validate fails startup — the safety net.
  6. Write a V2 migration adding an index or a column, restart, and confirm only the new migration runs.

Official documentation

Next: the persistence context — the first-level cache and entity lifecycle.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship