Self-upgrading Java Enterprise applications 1


Updating Java Enterprise applications can be a complex task. Typically you’d perform one or more steps from this list:

  • determining what database schema version you are running in the target environment
  • applying one or many SQL scripts for upgrading a schema (adding tables, columns, …)
  • running one or many migration scripts or binaries (e.g. for setting initial values of newly added columns if it’s a complex calculation that cannot be easily done in SQL)
  • capturing what „database“ version you are now running for a specific instance (e.g. database schema version 1.4.1.0 in production, 1.5.0.0 in development, …)
  • updating your applications binaries

Often, the order of these steps is crucial and there is some risk of breaking things.

While I will not cover updates of the applications binaries (in an ideal world, just drop the new EAR into the application servers deployment directory), I will focus on easy upgrades related to the persistence layer.

Implementing migrations with Flyway

Flyway labels it self as „version control for your database“. This captures its main purpose, but it can be used for different use cases that reach beyond just pure database schema versioning.

Flyway supports SQL and Java migrations. By following a naming scheme, Flyway detects the sequence for running multiple migrations, as well as detects which migrations to run. The second part relies on a metadata table stored in the schema. It is named „flyway_schema_history“ and stores which migrations have already been run. During a Flyway migration run, the current database schema version is compared against all available migration artefacts. Then, all „open“ migrations are applied in a specific order.

SQL migrations

To add SQL migration steps, just put your .sql file in a package / folder „db.migration“ in the classpath. You need to comply with a naming convention, e.g.

V1_10_0_0__UpdateDbVersion
V1_10_0_1__CreateTableCustomers
V1_10_0_2__AddIndexes

Java migrations

Similar to SQL migrations, put Java classes in the same package, following a similar naming convention. If you want to run Java migrations right after your SQL migrations, you could continue like this:

V1_10_0_3__RecalculateHashes.java
V1_10_0_4__Validate.java

You are free to mix SQL and Java migrations in whatever sequence you need.

Java migrations need to implement a specific Flyway interface or extend BaseJavaMigration. Here’s a sample migration:

package db.migration;

import java.sql.PreparedStatement;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.jboss.logging.Logger;

public class V1_10_0_3__RecalculateHashes extends BaseJavaMigration {

public void migrate(Context context) throws Exception {

 PreparedStatement statement =
 context.getConnection().prepareStatement("INSERT INTO test_table (somecolumn) VALUES ('test migration')");

 try {
     statement.execute();
 } finally {
     statement.close();
 }
}
}

Executing migrations

Migrations can be executed as part of a build process, by using a command line client or using the Flyway Java API. As this article is about „self upgrading“ applications, I will only cover the Java API here.

Starting the Flyway migration process is as easy as this:

Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:/johndoedb");
Flyway flyway = Flyway.configure().dataSource(ds).load();
flyway.migrate();

Look up your datasource using JNDI, hand it over to Flyway and call #migrate. That’s it.

Well, only if you start with Flyway from the very first version of your application.

Baselining your database schema

If you already have an application that is deployed and running in production, you need to tell Flyway what the „baseline“ version is. Sticking to our examples above, we introduce database version control with version the application version 1.10. Our latest version running in production is 1.9.1, so this is our baseline. Flyway can then be configured like this:

Flyway flyway = Flyway.configure().dataSource(ds).baselineVersion("1.9.1.0").baselineOnMigrate(true).load();
flyway.migrate();

Upon next execution of the migration process, Flyway will create the metadata table and call your migrations.

Triggering schema migrations in a Java Enterprise application

Now that we know how to develop migrations and how to call them via the Flyway Java API, we just need to find the right „trigger“ in a Java Enterprise application that makes use of an ORM, e.g. Hibernate. When an application server deploys an application, it will also validate the database schema based on the JPA entities that are shipped as part of your application. This will happen before your EJBs are deployed and started.

Therefore, using an EJB with the @Startup annotation is too late for calling your migrations.

While there might be different solutions to this problem, I have decided to use a Hibernate Integrator. It will be called before Hibernate validates the database schema.

An Integrator could look like this:

public class DatabaseMigrator implements Integrator {

private static final String lock = "lockObject";

@Override
public void integrate(Configuration c, SessionFactoryImplementor sfi, SessionFactoryServiceRegistry sfsr) {
    synchronized (lock) {
        try {
            Context ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup("java:/johndoedb");

            Flyway flyway = Flyway.configure().dataSource(ds).baselineVersion("1.9.1.0").baselineOnMigrate(true).load();
            flyway.migrate();

        } catch (Throwable me) {
            log.error("exception caught", me);
        }
    }
}

}

Put this file in a JAR of your EAR. The JAR then needs to have a file „org.hibernate.integrator.spi.Integrator“ (yes, use this as the file name) under its META-INF/services folder. The content of this file is the full class name of your Integrator class, e.g.

org.myapplication.persistence.DatabaseMigrator

Now, when launching your application server, it will

  • find that there is a Hibernate Integrator to be run before deploying your application
  • launch the Integrator, which then
  • calls Flyway to execute migrations
  • deploy your application

You are now able to skip version during deployment. E.g. going from 1.10 to 1.23 is no different from going from 1.10 to 1.11.

Here’s sample log of an EAR deployed on Wildfly:

22:20:39,957 INFO [org.flywaydb.core.internal.license.VersionPrinter] (ServerService Thread Pool -- 85) Flyway Community Edition 5.2.1 by Boxfuse
22:20:39,966 INFO [org.flywaydb.core.internal.database.DatabaseFactory] (ServerService Thread Pool -- 85) Database: jdbc:mysql://localhost:3306/jlawyerdb (MySQL 5.7)
22:20:40,005 INFO [org.jboss.as.jpa] (ServerService Thread Pool -- 89) WFLYJPA0010: Starting Persistence Unit (phase 2 of 2) Service 'j-lawyer-server.ear/j-lawyer-server-ejb.jar#j-lawyer-server-ejbPU'
22:20:40,017 INFO [org.hibernate.dialect.Dialect] (ServerService Thread Pool -- 89) HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
22:20:40,031 INFO [org.flywaydb.core.internal.command.DbValidate] (ServerService Thread Pool -- 85) Successfully validated 2 migrations (execution time 00:00.037s)
22:20:40,044 INFO [org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory] (ServerService Thread Pool -- 85) Creating Schema History table: `jlawyerdb`.`flyway_schema_history`
22:20:40,061 INFO [org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory] (ServerService Thread Pool -- 89) HHH000397: Using ASTQueryTranslatorFactory
22:20:40,098 INFO [org.flywaydb.core.internal.command.DbBaseline] (ServerService Thread Pool -- 85) Successfully baselined schema with version: 1.9.1.0
22:20:40,105 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 85) Current version of schema `jlawyerdb`: 1.9.1.0
22:20:40,106 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 85) Migrating schema `jlawyerdb` to version 1.10.0.0 - UpdateDbVersion
22:20:40,114 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 85) Migrating schema `jlawyerdb` to version 1.10.0.1 - SampleMigration
22:20:40,115 INFO [db.migration.V1_10_0_1__SampleMigration] (ServerService Thread Pool -- 85) Running migration db.migration.V1_10_0_1__SampleMigration
22:20:40,121 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 85) Successfully applied 2 migrations to schema `jlawyerdb` (execution time 00:00.021s)
22:20:40,123 INFO [stdout] (ServerService Thread Pool -- 89) Starting j-lawyer.org database migrations...
22:20:40,124 INFO [org.flywaydb.core.internal.database.DatabaseFactory] (ServerService Thread Pool -- 89) Database: jdbc:mysql://localhost:3306/jlawyerdb (MySQL 5.7)
22:20:40,152 INFO [org.flywaydb.core.internal.command.DbValidate] (ServerService Thread Pool -- 89) Successfully validated 3 migrations (execution time 00:00.024s)
22:20:40,156 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 89) Current version of schema `jlawyerdb`: 1.10.0.1
22:20:40,157 INFO [org.flywaydb.core.internal.command.DbMigrate] (ServerService Thread Pool -- 89) Schema `jlawyerdb` is up to date. No migration necessary.
22:20:40,238 INFO [org.hibernate.tool.hbm2ddl.SchemaValidator] (ServerService Thread Pool -- 85) HHH000229: Running schema validator

I noticed the Hibernate integrator might be called twice, so I put a synchronization in place. The second run would detect that the schema is up to date and would do nothing.

Next Steps

There is a lot more that Flyway can do, e.g. supporting rollbacks. Check out the docs for more information.


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Ein Gedanke zu “Self-upgrading Java Enterprise applications

  • j-dimension.com Autor des Beitrags

    Just a note on security: running your apps with privileges to perform schema changes can be considered a security risk. You’ll either have to live with this, or grant privileges just for the time of the upgrade, or call the migrations as part of a build / deployment script (= not as part of your application).