It is important to develop a well-designed schema at the beginning of the project, but with the new requirements, changing your initial schema is hard to avoid. These changes must be carefully applied to avoid compromising existing data but also to meet new expectations. Migration group changes that modify database structure. It allows controlled transition from their current state to a new desired state (e.g., adding, deleting columns or tables).

One of the commonly used open-source database migrations tools is Flyway. It updates a database from one version to the next using migrations. For a single migration, all statements are run within a single database transaction. Migrations can be written in SQL or Java for advanced database transformations. Flyway’s SQL-script based migrations are good enough for most use cases. Java-based migrations provide an easy and powerful way to implement requested logic to adapt existing data in case SQL script cannot be used. Flyway automatically finds and executes SQL scripts as well as migration classes.

CREATE TABLE demo_user (
    id UUID PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

Simple SQL migration V1__init.sql

Java-based migrations must implement the JavaMigration interface. However, it is easier to extend BaseJavaMigration abstract class as it encourages Flyway’s default naming convention, enabling Flyway to automatically extract the version and the description from the class name. It only remains to implement the interface. Class name should follow Flyway’s naming schema <Prefix><Version>__<Description>.java. When migrations are being run, Flyway detects the current database version, scans for all SQL and Java migrations, and executes the required ones.

Mentioned naming schema parts have different interpretations:

  • Prefix – V for versioned migrations, U for undo migrations, R for repeatable migrations
  • Version – Migration version number. Major and minor versions may be separated by an underscore. Underscores are automatically replaced by dots at runtime.
  • Description – Textual description of the migration. A double underscore separates the description from the version numbers. Single underscores (automatically replaced by spaces at runtime) separate the words.
@Component
public class V2__trigger_java_migration extends BaseJavaMigration {

    @Override
    public void migrate(Context context) {
        System.out.println("Triggering V2 migration...");
    }
}

Simple Java-based migration

As part of the migration run, Flyway performs change detection validation using a migration checksum. Java-based migrations, by default, do not have a checksum, so to be considered for the mentioned validation, getChecksum()method must be implemented.

When using Flyway in combination with Spring Boot and Hibernate, auto-configuration ensures that database migrations have run before Hibernate is initialized. That is, one should not rely on Flyway auto-configuration and use Flyway to populate tables created by Hibernate. The solution is to use Flyway to both create and populate tables, along with switching off Hibernate’s table creation: spring.jpa.hibernate.ddl-auto=none

Another solution is to disable Flyway auto-configuration: flyway.enabled=false. With this, it is possible to adjust the configuration to support the requested changes. Flyway initialization time can be changed, and one useful use case is depending on Hibernate. Another use case that may come in handy is using Dependency Injection and already defined application beans, running Java migrations after application startup. This approach requires defining component with custom Flyway configuration that will pick up both SQL and Java migrations.

@Component
public class FlywayConfiguration implements CommandLineRunner {

    private JavaMigration[] javaMigrations;
    private DataSource dataSource;

    public FlywayConfiguration(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Autowired
    public void setJavaMigrations(JavaMigration[] javaMigrations) {
        this.javaMigrations = javaMigrations;
    }

    @Override
    public void run(String... args) {
        Flyway flyway = Flyway.configure()
                .dataSource(dataSource)
                .javaMigrations(javaMigrations)
                .load();

        flyway.migrate();
    }
}

Flyway custom configuration

After configuring Flyway initialization time, Java migration class can use service methods, etc. Note: this feature is available with importing flyway-core dependency version 6.0.0 and higher.

@Component
public class V3__insert_demo_user extends BaseJavaMigration {
    private final DemoUserService demoUserService;

    public V3__insert_demo_user(DemoUserService demoUserService) {
        this.demoUserService = demoUserService;
    }

    @Override
    public void migrate(Context context) throws Exception {
        demoUserService.createDemoUser("John Doe");
    }
}

Java-based migration using Dependency Injection

Flyway keeps track of all created migrations, marking them as pending, successful, or failed. All this information (and much more) provides an automatically created flyway_schema_history table. This way, all database versioning, and made changes are being monitored in one place. 

version description type checksum execution time success
1 init SQL -899440118 7 true
2 trigger java migration JDBC [null] 2 true
3 insert demo user JDBC [null] 92 true

Relevant information about executed migrations

Interested in more?

Click here for Atlantbh Blogs and Success Stories about Software Development.

Leave a Reply