Spring Boot - Flyway

This article is about Spring Boot’s Flyway integration. Flyway is a tool for automating DB schema migration. It is open-source and favours simplicity and convention over configuration principles.

Flyway solves the issue of keeping track and executing database schema migrations. If you are using Hibernate, you might have heard or you might be using the hibernate.hbm2ddl.auto setting with update. This setting will automatically export updates to the database when the Hibernate SessionFactory is created. Relying on this setting is not favourable for production environments though as it might suffer from inflexibility in certain use-cases. Consider you need to run an SQL statement for pre-filling a newly created table. That’s not something that can be done with Hibernate schema migrations.

Flyway is simple and flexible enough to provide us ways how to handle more advanced db migrations. On top of that, with the introduction of such a schema migration tool you automatically pre-define a way and format how db migration steps need to be specified and possibly committed into your version control system of choice.

Compared to Liquibase we prefer Flyway as it offers a more “down-to-SQL” approach which feels super simple, instead of providing a meta-data DSL for specifying the migration steps.

Adding Flyway

Adding Flyway to Spring Boot is just a matter of adding another dependency in your build.gradle file:

dependencies {
    // ...
    
    // Flyway support
    compile 'org.flywaydb:flyway-core:4.2.0'
}

The most current version of Flyway is 4.2.0. Once added in the build.gradle file, Spring Boot will automatically detect the Flyway classes on its classpath and will auto configure it. In Spring Boot, there is a class called FlywayAutoConfiguration in package org.springframework.boot.autoconfigure.flyway being part of the spring-boot-autoconfigure dependency/JAR file which does this job.

For certain Flyway commands you will also need to add Flyway Gradle support to your build.gradle or you need to install the Flyway command-line client.

Per default, the newly added dependency will auto configure Flyway with the following settings (documented here):

# FLYWAY (FlywayProperties)
flyway.baseline-description= #
flyway.baseline-version=1 # version to start migration
flyway.baseline-on-migrate= #
flyway.check-location=false # Check that migration scripts location exists.
flyway.clean-on-validation-error= #
flyway.enabled=true # Enable flyway.
flyway.encoding= #
flyway.ignore-failed-future-migration= #
flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it.
flyway.locations=classpath:db/migration # locations of migrations scripts
flyway.out-of-order= #
flyway.password= # JDBC password if you want Flyway to create its own DataSource
flyway.placeholder-prefix= #
flyway.placeholder-replacement= #
flyway.placeholder-suffix= #
flyway.placeholders.*= #
flyway.schemas= # schemas to update
flyway.sql-migration-prefix=V #
flyway.sql-migration-separator= #
flyway.sql-migration-suffix=.sql #
flyway.table= #
flyway.url= # JDBC url of the database to migrate. If not set, the primary configured data source is used.
flyway.user= # Login user of the database to migrate.
flyway.validate-on-migrate= #

It is important to note that per default, Flyway will be executing migrations against the default data source. If you have more data sources configured, you need to add the @FlywayDataSource annotation as detailed in the Spring Boot documentation.

As you can see, the only two properties being preconfigured are flyway.baseline-version and flyway.locations. In Flyway the database migrations are specified in SQL files. The default location for those migration files is src/main/resources/db/migration. In this directory you can place files following this naming scheme: V<version number>__<description>.sql. The version number will basically be a positive number which will have to be incremented by the database migration script author with every database migration. flyway.baseline-version configures the initial version number to be 1, so your first database migration file will start with V2__<description>.sql.

On startup of the Spring Boot application, Flyway will look into db/migration and executed all database migration files which have not yet been executed. In order to keep track of the executions, it will initially create a table called schema_version in the target database.

This initial creation is called “baselining”. It will create the schema_version table and will assume that the target database is in the most current state, it will not execute any database migration files around found in db/migration. However, baselining is something that we have to trigger, it is not done automatically.

As mentioned above, baseline is such a command that we have to do via the Gradle integration (and therefore a Gradle tasks) or the Flyway command-line client. In our case, we use the Gradle integration:

plugins {
    id "org.flywaydb.flyway" version "4.2.0"
}

Besides activating the Flyway Gradle plugin, we also have to specify the database connection properties in our build.gradle file:

flyway {
    url = 'jdbc:h2:mem:mydb'
    user = 'myUsr'
    password = 'mySecretPwd'
    schemas = ['schema1', 'schema2', 'schema3']
    placeholders = [
        'keyABC': 'valueXYZ',
        'otherplaceholder': 'value123'
    ]
}

More information on configuring Flyway with Gradle can be found here.

Once we have configured our Flyway integration, we can run baseline against our target database. With Gradle, this is done by executing the flywayBaseline task. After it was successfully executed, we will have a new schema_version table in our database that will actually hold only one row of type BASELINE. This is the starting point for database migrations.

Hint: do not forget to switch your hibernate.hbm2ddl.auto setting to none instead of update or any other option which would cause the target database to be modified.

Adding Migrations

Let’s say we have a class Car and we want to add a version column for activating optimistic locking for this JPA entity. We would simply add another instance variable called version:

import javax.persistence.Entity;
import javax.persistence.Version;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class Car { 

    // ...
    
    @Version
    private Integer version;

    // ...
}

However, as we have turned off Hibernate’s schema migration with hibernate.hbm2ddl.auto=none, we need to add the VERSION column ourselves. First step is to create a new database migration file and place it in db/migration. As it is our first database migration and the baseline version starts with 1 (according to the default config values), we need to call it V2__add_version_to_car.sql. The add_version_to_car can be completely arbitrary text, important is the version number after V. Inside this SQL file, we can use arbitrary statements for our target database. Flyway supports a wide range of databases, a full list can be found here.

For the matter of simplicity, we use H2 in our example above. To add the VERSION column we can simply say in V2__add_version_to_car.sql:

ALTER TABLE CAR ADD COLUMN VERSION INT;
UPDATE CAR SET VERSION = 0;

The 2nd SQL statement is important. If we already have entries in our CAR table we need to make sure that the new column will get prefilled with 0.

That’s it for now. When we start our Spring Boot application again, Flyway will - based on the schema_version table - detect there is one database migration pending and will automatically executed it during startup. Appropriate logs will appear in the console or log file of choice.

There is one important thing to mention: once a database migration has been executed, it can not be changed and executed again without manual changes on the schema_version table. When you have a look at the schema_version table, you will see that it contains a column for keeping track of the checksum of the executed statement. When the statement is changed, Flyway will complain that the checksum has changed and your application will not start up. Best practice is to revert the change in another database migration step.

More Configuration

So far, we were running with the default settings provided by Spring Boot’s auto configuration mechanism. It is important to note though, that those configuration settings can of course be overridden by the application. For example, the flyway.locations configuration value supports a special variable {vendor} which can be used in the directory location for the specific database type. In our case, the database type is H2 so we could rewrite flyway.locations to

flyway.locations=classpath:db/migration/{vendor}/

and Flyway would look in db/migration/h2/ for database migration scripts when running with H2. If you are using multiple Spring environments/profiles, you could of course set the flyway.locations configuration setting depending on the currently active profile.

More Commands

In this article we used basically two Flyway commands: migrate and baseline. However, there are four more commands which might come in handy in certain situations (clean, info, validate and repair). For a more detailed look at them, please go through the Flyway documentation.