Database migration tool that enables versioned database schema evolution with simple SQL scripts and Java migrations
—
Framework for creating programmatic database migrations using Java code, providing full access to database connections and configuration for complex migration scenarios that require procedural logic.
Core contract for Java-based migrations providing version, description, and execution methods.
/**
* Interface to be implemented by Java-based migrations.
*/
public interface JavaMigration {
/** Get the version of this migration */
MigrationVersion getVersion();
/** Get the description of this migration */
String getDescription();
/** Get the checksum of this migration. Can be null if no checksum is available */
Integer getChecksum();
/** Whether the execution should take place inside a transaction */
boolean canExecuteInTransaction();
/** Execute the migration */
void migrate(Context context) throws Exception;
}Base implementation providing default behavior and automatic version extraction from class names.
/**
* Convenience class for Java-based migrations. Automatically extracts the version and description from the class name.
*/
public abstract class BaseJavaMigration implements JavaMigration {
/** Get the version from the migration class name */
public final MigrationVersion getVersion();
/** Get the description from the migration class name */
public String getDescription();
/** Get the checksum. Default implementation returns null */
public Integer getChecksum();
/** Whether this migration can execute within a transaction. Default is true */
public boolean canExecuteInTransaction();
/** Execute the migration - must be implemented by subclasses */
public abstract void migrate(Context context) throws Exception;
}Usage Examples:
// Version and description extracted from class name: V2_1__Create_user_table
public class V2_1__Create_user_table extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (Statement statement = context.getConnection().createStatement()) {
statement.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100))");
}
}
}
// Custom checksum and transaction control
public class V3__Complex_migration extends BaseJavaMigration {
@Override
public Integer getChecksum() {
return 12345; // Custom checksum for this migration
}
@Override
public boolean canExecuteInTransaction() {
return false; // Execute outside transaction for PostgreSQL DDL
}
@Override
public void migrate(Context context) throws Exception {
// Complex migration logic here
}
}Execution context providing access to database connection and configuration during migration execution.
/**
* The context relevant to a migration.
*/
public interface Context {
/** Get the configuration currently in use */
Configuration getConfiguration();
/** Get the JDBC connection to use to execute statements */
Connection getConnection();
}Usage Examples:
public class V4__Data_migration extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Configuration config = context.getConfiguration();
Connection connection = context.getConnection();
// Use configuration
String[] schemas = config.getSchemas();
boolean createSchemas = config.isCreateSchemas();
// Execute migration
try (PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO users (name, created_date) VALUES (?, ?)")) {
stmt.setString(1, "Migration User");
stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
stmt.executeUpdate();
}
}
}public class V5__Migrate_user_data extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Connection conn = context.getConnection();
// Read from old table
try (Statement select = conn.createStatement();
ResultSet rs = select.executeQuery("SELECT * FROM old_users");
PreparedStatement insert = conn.prepareStatement(
"INSERT INTO new_users (name, email, status) VALUES (?, ?, ?)")) {
while (rs.next()) {
insert.setString(1, rs.getString("username"));
insert.setString(2, rs.getString("email_addr"));
insert.setString(3, "ACTIVE");
insert.addBatch();
}
insert.executeBatch();
}
// Drop old table
try (Statement drop = conn.createStatement()) {
drop.execute("DROP TABLE old_users");
}
}
}public class V6__Conditional_index extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Connection conn = context.getConnection();
// Check if index already exists
boolean indexExists = false;
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT COUNT(*) FROM information_schema.statistics " +
"WHERE table_name = ? AND index_name = ?")) {
stmt.setString(1, "users");
stmt.setString(2, "idx_user_email");
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next() && rs.getInt(1) > 0) {
indexExists = true;
}
}
}
if (!indexExists) {
try (Statement create = conn.createStatement()) {
create.execute("CREATE INDEX idx_user_email ON users(email)");
}
}
}
}public class V7__Environment_specific extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Configuration config = context.getConfiguration();
Connection conn = context.getConnection();
// Get environment from system property or default
String environment = System.getProperty("flyway.environment", "development");
// Different behavior per environment
String tableName = "development".equals(environment) ? "test_data" : "production_data";
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE " + tableName + " (id SERIAL, data TEXT)");
if ("development".equals(environment)) {
// Insert test data in development
stmt.execute("INSERT INTO " + tableName + " (data) VALUES ('test1'), ('test2')");
}
}
}
}public class V8__Batch_update extends BaseJavaMigration {
private static final int BATCH_SIZE = 1000;
@Override
public void migrate(Context context) throws Exception {
Connection conn = context.getConnection();
try (PreparedStatement update = conn.prepareStatement(
"UPDATE large_table SET processed = ? WHERE id = ?");
Statement select = conn.createStatement();
ResultSet rs = select.executeQuery("SELECT id FROM large_table WHERE processed IS NULL")) {
int count = 0;
while (rs.next()) {
update.setBoolean(1, true);
update.setLong(2, rs.getLong("id"));
update.addBatch();
if (++count % BATCH_SIZE == 0) {
update.executeBatch();
conn.commit(); // Commit in batches
}
}
// Execute remaining batch
if (count % BATCH_SIZE != 0) {
update.executeBatch();
}
}
}
}Java migration classes must follow specific naming patterns for automatic version and description extraction:
Versioned migrations: V<version>__<description>.java
V1_2_3__Create_user_table.java1.2.3Create user tableRepeatable migrations: R__<description>.java
R__Update_views.javaUpdate viewsNaming Examples:
// Version 1.0 - Initial setup
public class V1__Initial_setup extends BaseJavaMigration { }
// Version 2.1 - Add user table
public class V2_1__Add_user_table extends BaseJavaMigration { }
// Version 10.5.3 - Complex version
public class V10_5_3__Database_refactoring extends BaseJavaMigration { }
// Repeatable migration - runs on every migrate if changed
public class R__Update_calculated_fields extends BaseJavaMigration { }public class V9__Error_handling extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Connection conn = context.getConnection();
try (Statement stmt = conn.createStatement()) {
// Attempt migration
stmt.execute("ALTER TABLE users ADD COLUMN new_field VARCHAR(50)");
} catch (SQLException e) {
// Check if column already exists
if (e.getMessage().contains("duplicate column name") ||
e.getMessage().contains("already exists")) {
// Column exists, ignore error
return;
}
// Re-throw other errors
throw e;
}
}
}JavaMigrationmigrate() method called with execution contextJava migrations integrate seamlessly with SQL migrations, following the same versioning and execution order rules.
Install with Tessl CLI
npx tessl i tessl/maven-org-flywaydb--flyway-core