How to use Flyway for database migration in Spring Boot

        In this article, we'll see how to use Flyway to manage SQL database schemas in Spring Boot applications.

        In this article, we'll see how to use Flyway to manage SQL database schemas in Spring Boot applications.
Flyway is a database migration tool that provides migration history and rollback functionality, and allows us to separate the application's database schema-related layer from the database entity layer.

application settings

The Spring Boot application we will be using can be generated using this Spring Initializr link. It contains all necessary dependencies.
After downloading the application and resolving dependencies, we will create a new Postgres database called spring-boot-flyway and configure the application to connect to it.

Listing 2.1  application.properties :

spring.datasource.url=jdbc:postgresql://localhost:5432/spring-boot-flyway
spring.datasource.username=demo
spring.datasource.password=demo

By default, Flyway searches the db/migration/ directory in the classpath for migration files containing SQL statements for managing database tables and records. 

For older versions of the library, we may need to create an empty text file called .keep in resources/db/migration/  to ensure that this directory is compiled and available during application startup to avoid errors.

With this done, we can now start the application and it should run successfully.

basic usage

The way Flyway works is that we create a migration file in the resources/db/migration directory and Spring Boot automatically executes the migration script since we added the Flyway dependency to the classpath in Section 2 .

Listing 3.1  V1__Users.sql :

CREATE TABLE IF NOT EXISTS users
(
    id    SERIAL,
    email VARCHAR(200) NOT NULL,
    name  VARCHAR(200) NOT NULL,
    PRIMARY KEY (id)
);

Let's take a moment to examine the code snippet in Listing 3.1. The file name V1__Users.sql follows certain conventions:

  • V  " indicates that this is a versioned migration.
  • The " 1  " after  the V is the actual version number. It can also be "  V1_1  ", which will translate to version 1.1.
  • It is followed by the separator "  __  " (two underscores). This separates the version information from the name of the migration file (Users in this case ).
  • The last part "  .sql  " is the extension; therefore, the file contains a simple SQL statement.

At this point, restarting the application will create the user table in the database. Also, we can see that there is another table that we didn't create explicitly -  Flyway_schema_history  .

Flyway_schema_history is used by Flyway itself to track applied migrations . If this table is missing, Flyway will assume we are initializing the database for the first time and run all migrations in version number order.

When the Flyway_schema_history table exists, Flyway will only apply newer migration files that have not been applied before. This means, in order to add a new table, we just need to create an updated migration file with an updated version number and restart the application.

Instead of using SQL, we can also write migration scripts in Java . In the Java migration style, our migration files are Java classes that must extend abstract BaseJavaMigrationclasses and implement migratemethods.

IDEs generally don't expect Java classes to be in the resources directory, so we'll create a new package called db/migration in src/main/java . It is very important to know that this new package db/migration should be located in the src/main/jav directory .

Let's create a new Java migration to add the new table:

Listing 3.2  V2__Posts.java  :

public class V2__Posts extends BaseJavaMigration {
    @Override
    public void migrate(Context context) throws Exception {
        var sql = """
                CREATE TABLE posts (
                     id SERIAL,
                     author_id INT NOT NULL,
                     post TEXT NOT NULL,
                     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                     PRIMARY KEY (id)
                );
                """;
        try(var statement = context.getConnection().createStatement()) {
            statement.execute(sql);
        }
    }
}

The advantage of using Java migrations over SQL files is that we can add custom logic, conditions and validations that are not possible with plain SQL. For example, we can check if another table exists or get some value from the environment etc.

As you may have guessed by now, yes, it is possible to mix SQL and Java style migrations in the same code base, as long as we ensure that the Flyway location is the same in both cases.

Flyway configuration and customization

So far we've been using the default Flyway behavior. We can further tune Flyway to suit our needs. For example, we can change the default location of migration files, configure the database schema (aka tablespace), change the SQL migration prefix from "V" to whatever we want, and more.

In the configuration below, we configure the path where the migration files are located and disable cleaning the database (i.e. dropping all tables) to prevent accidental use in production.

Listing 4.1  application.properties:

spring.flyway.locations=classpath:migrations
spring.flyway.clean-disabled=true

There are other configurable properties under this key spring.flywaythat we can use to fine tune the behavior of the library. Also, we can check out the Flyway documentation page for reference.

flight path callback

Flyway provides us with the ability to configure callbacks that can be invoked at different stages of the migration process. The callback mechanism is a convenient way to perform certain actions at different stages of the migration lifecycle. 

Let's say we have some default data that we want to seed on application startup. We can simply create a AFTER_MIGRATEcallback that supports this event.

Listing 5.1  FlywayDatabaseSeeder.java:

public class FlywayDatabaseSeeder implements Callback {

    @Override
    public boolean supports(Event event, Context context) {
        return event.name().equals(Event.AFTER_MIGRATE.name());
    }
    
    @Override
    public void handle(Event event, Context context) {
        try(var statement = context.getConnection().createStatement()) {

            var ADMIN_EMAIL = "[email protected]";
            
            var checkQuery = "SELECT id FROM users WHERE email = %s"
                    .formatted(ADMIN_EMAIL);
            
            statement.execute(checkQuery);
            ResultSet resultSet = statement.getResultSet();
            resultSet.last();

            //return if the seeder has already been executed
            if(resultSet.getRow() >= 0) return;

            var sql = """
                    INSERT INTO users (email, name) VALUES
                    ('%s', 'Super Admin')
                    """.formatted(ADMIN_EMAIL);
            
            statement.execute(sql);
            
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean canHandleInTransaction(Event event, Context context) {
        return true;
    }

    @Override
    public String getCallbackName() {
        return FlywayDatabaseSeeder.class.getName();
    }
}

In the listing above, in supportsthe method, we declare that AFTER_MIGRATEthis callback should only be executed for the event, and in handlethe method, we outline the logic to insert the default superadmin user if it doesn't already exist.

Before that, we need to register the callback class with Flyway in SpringBoot. We FlywayMigrationStrategydo this by creating a bean.

Listing 5.2  SpringBootFlywayApplication.java  :

@Bean
public FlywayMigrationStrategy flywayMigrationStrategy() {
	return (flywayOld) -> {

		/*
		 Update the existing autoconfigured Flyway
		 bean to include our callback class
		*/
		Flyway flyway = Flyway.configure()
				.configuration(flywayOld.getConfiguration())
				.callbacks(new FlywayDatabaseSeeder())
				.load();

		flyway.migrate();

	};
}

There are other events in the rg.flywaydb.core.api.callback.Event  enumeration , we can configure Callbackthe class to support. For example, you could have a callback to support the AFTER_MIGRATE_ERRORevent and send a Slack notification to alert the engineer.

tips and tricks

When developing in a local environment, you can remove migration entries from the Flyway_schema_history table.

The next time you start the app, the migration whose history you deleted will be executed again. This way, you can correct errors or update the schema while still developing on your local machine without deleting the entire database.

Also, in SpringBoot, you can control when Flyway executes migration scripts on application startup. For example, suppose we don't want to automate migrations in our local environment. We can do the following:

Listing 6.1  SpringBootFlywayApplication.java:

@Bean
public FlywayMigrationStrategy flywayMigrationStrategy(@Value("${spring.profiles.active}") String activeProfile) {
	return (flywayOld) -> {

		/*
		 Update the existing autoconfigured Flyway
		 bean to include our callback class
		*/
		Flyway flyway = Flyway.configure()
				.configuration(flywayOld.getConfiguration())
				.callbacks(new FlywayDatabaseSeeder())
				.load();

		if(!"local".equalsIgnoreCase(activeProfile)) {
			flyway.migrate();
		}

	};
}

in conclusion

One of the advantages of using a database migration tool is that it makes the database schema part of the application code base. Since there is a central point of reference in the application, it is easier to track changes to the database over time.

Guess you like

Origin blog.csdn.net/qq_28245905/article/details/132290264