Dependency Injection with Flyway
What is Flyway
Flyway is an open-source database version control tool implemented in Java. It offers various methods such as CLI, Java API, Maven/Gradle Plugin, etc., making it convenient for developers to manage and deploy changes to database table structures and contents in traceable code form. Flyway supports most mainstream relational databases, including MySQL, SQL Server, Oracle Database, PostgreSQL, SQLite, TiDB, MariaDB, etc. Support for MongoDB is still in the preview stage (Mongock is recommended instead). Its counterpart, Liquibase, is more complex to use compared to Flyway, with additional concepts serving as a learning curve.
Integration with Spring Boot
Following Spring Boot's well-known convention-over-configuration principle, the Spring team provides Flyway Auto Configuration.
@AutoConfiguration(after = { DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class,HibernateJpaAutoConfiguration.class })
@ConditionalOnClass(Flyway.class)
@Conditional(FlywayDataSourceCondition.class)
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true)
@Import(DatabaseInitializationDependencyConfigurer.class)
@ImportRuntimeHints(FlywayAutoConfigurationRuntimeHints.class)
public class FlywayAutoConfiguration { /*...*/ }
The fully qualified class name org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
will perform automatic configuration when the following conditions are detected:
- The
Flyway
class exists (class name:org.flywaydb.core.Flyway
) - Bean or properties conditions are met in the
FlywayDataSourceCondition
class, meaning connection configurations such asDataSource
and database URL exist. - The property configuration
spring.flyway.enabled=true
exists.
For MySQL, besides the required JDBC dependency, you need to include Flyway's own dependencies. Spring Boot has already declared the version number in the POM, so no additional version field is needed here:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
There are two ways to use it:
- Native SQL
- Java API
Flyway will discover version change files or implementation classes based on the default configured folder path classpath:db/migration
.
V{VERSION}__{DESCRIPTION}.sql
V{VERSION}__{DESCRIPTION}.java
VERSION
can be a string version containing dots and underscores, and DESCRIPTION
is a simple description text. The separator in the middle is two underscores. Custom prefixes and suffixes can be configured through spring.flyway.
The V{VERSION}__{DESCRIPTION}.sql
file needs to be placed in the src/main/resources/db/migration
folder, while Java classes need to define the package name as db.migration and inherit the superclass BaseJavaMigration
. We need to do is overriding the following method:
void migrate(Context context) throws Exception;
According to the official documentation, we can implement database content version changes using Java code in the migrate()
method. However, the drawback here is that we cannot perform dependency injection for V{VERSION}__{DESCRIPTION}.java
in the Spring environment. We can only attempt to use plain SQL implementations through native JDBC methods, static methods, etc., rather than fully utilizing existing DAO interfaces for business data updates. Therefore, custom configuration of Flyway is required.
Performing Dependency Injection into Flyway Class
package me.lawrenceli.migration.config;
import jakarta.annotation.PostConstruct;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.migration.JavaMigration;
import org.flywaydb.core.api.output.MigrateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class FlywayConfig {
// Custom migration history table name
private static final String SCHEMA_HISTORY_TABLE = "schema_changes";
@Autowired
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@PostConstruct
public void migrate() {
log.info("Flyway, starting!");
// Retrieving all migration implementation classes through the Spring container
// All implementation classes no longer need to be defined in the `db.migration` package,
// they can be placed in any location that supports Bean scanning
JavaMigration[] migrationBeans = applicationContext
.getBeansOfType(JavaMigration.class)
.values()
.toArray(new JavaMigration[0]);
Flyway flyway = Flyway.configure()
.dataSource(dataSource) // No need to configure flyway JDBC URL by using the original DataSource
.locations("db/migration") // Default migration script path
.table(SCHEMA_HISTORY_TABLE) // Default migration history table is `flyway_schema_history`
.baselineOnMigrate(true) // Default false, must be set to true for initial migration on existing databases
.baselineVersion("0") // Default "1"
.executeInTransaction(true) // Performing migration as a transaction
.installedBy(applicationContext.getId()) // Using microservice name as migration executor
.javaMigrations(migrationBeans) // Registering migration classes
.load();
MigrateResult migrate = flyway.migrate(); // Executing migration by calling subclass implementations sequentially
log.info("Flyway migrated {} versions. {}", migrate.migrationsExecuted, migrate.success);
}
}
Since Flyway's configuration is based on manual configuration like this, it's necessary to exclude the original auto-configuration classes in the Spring Boot startup class to avoid conflicts in auto-configuration loading.
@SpringBootApplication(exclude = FlywayAutoConfiguration.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Finally, define a Component Bean to implement BaseJavaMigration
:
package me.lawrenceli.balabala.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class V2__QueryExample extends BaseJavaMigration {
@Autowired
private MyMapper myMapper; // Bean of DAO
@Override
public void migrate(Context context) throws Exception {
Data data = myMapper.selectById(2024L);
// ... other CRUD codes with Java
}
}
Now, all migration classes can easily use dependency injection to perform CRUD operations. In practice, Flyway will execute all migrations synchronously after database connection configuration and before HTTP service exposure, ensuring that timing does not affect online services.