Integrating Flyway with Java EE and using its datasource

Integrating Flyway with Java EE and using its datasource

Flyway is my favourite tool when it comes to database migrations because its convention over configuration approach makes it fairly easy to use while bootstrapping and configuration is reduced to a minimum. For those of you who don’t know anything about database migration tools (evolutionary database design) in short the concept is to track SQL scripts with your source code which reflect the actual version of your database that matches the code version to run properly. This could be done without any framework but what tools like Flyway or liquibase bring to the table is the ability to check automatically at build- or start-time if the database is at the latest available version and if not run all available SQL scripts from the actual database version to the newest one available. Flyway can be used with all the major SQL dialects, many different relational databases and has plugins for a huge variety of frameworks to be integrated into. For more detailed information visit the projects website.

As I’m a heavy Java EE user I ran into the problem that Java EE has no special support by Flyway. My problems with this are that there is no build in way to get the actual JPA datasource (defined in the persistence.xml) and there is no solution provided that wires Flyway in the startup process to run the migration scripts at startup. Flyway provides integration plugins with this functionalities for other frameworks like Spring Boot but fortunately it is not that complicated to realise the same thing in Java EE.

In many scenarios it is no problem to trigger Flyway in the right moment as it is possible to integrate it in the maven build process or start it manually via the command line but both solutions doesn’t fit my needs. I want to have Flyway check my database at startup as the production server is not the server where the build process happens and I want to avoid a situation where I have to execute a shell command manually after deploying a new version. For this cases Flyway can be started from the source code as well. As it has to be run before the application starts and tries using the database you have to make sure that the Flyway scripts are run before that. I found two solutions to make this work. The first one uses Hibernates “Service Provider Interface” (SPI) -I use WildFly in this example which comes with Hibernate- to register a new integrator and grab the datasource via a little bit of reflection code while the second approach (which is my favourited solution) defines a new singleton bean which uses @resource injection to get the datasource.

Hibernate SPI integrator solution

For this approach it is necessary to implement a new org.hibernate.integrator.spi.Integrator to register a new hibernate service.

public class FlywayIntegrator implements Integrator {

    private final Logger logger = LoggerFactory.getLogger(FlywayIntegrator.class);

    @Override
    public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory,
                          SessionFactoryServiceRegistry serviceRegistry) {

        logger.info("Migrating database to the latest version");

        final JdbcServices jdbcServices = serviceRegistry.getService(JdbcServices.class);
        Connection connection;
        DataSource dataSource = null;

        try {
            connection = jdbcServices.getBootstrapJdbcConnectionAccess().obtainConnection();
            final Method method = connection != null ? connection.getClass().getMethod("getDataSource", null) : null;
            dataSource = (DataSource) (method != null ? method.invoke(connection, null) : null);
        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | SQLException e) {
            e.printStackTrace();
        }

        Flyway flyway = new Flyway();
        flyway.setDataSource(dataSource);

        MigrationInfo migrationInfo = flyway.info().current();

        if (migrationInfo == null) {
            logger.info("No existing database at the actual datasource");
        }
        else {
            logger.info("Found a database with the version: {}", migrationInfo.getVersion());
        }

        flyway.migrate();
        logger.info("Successfully migrated to database version: {}", flyway.info().current().getVersion());
    }

    @Override
    public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
        // Not needed here
    }
}

The interesting part is in the lines 16-18. While the general connection interface doesn’t provide a possibility to get the actually used datasource, the Hibernate implementation of this interface comes with a getDatasource method which can be used via reflection as shown above. When we have the datasource the rest is very straightforward. The datasource is given to Flyway, some log statements are generated and flyway.migrate() triggers the migration on the actual datasource.

To make this work you have to register the new integrator with Hibernate. This is done by putting a file with the name org.hibernate.integrator.spi.Integrator in the folder src/main/resources/META-INF/services with the package and classname as it’s only content de.fnortheim.FlywayIntegrator. You can find an example of this solution on my GitHub account in the project flywayIntegrations/flywayJeeHibernateReflectionIntegration.

The advantage of this solution is that you don’t need to specify the datasource you want to use because you will get it per reflection. The downside is that you are highly dependent to Hibernate and its internal API which could and has been changed over time.

Singleton EJB solution

This approach uses just the mechanisms Java EE provides and no private APIs

@Singleton
@Startup
@TransactionManagement(value = TransactionManagementType.BEAN)
public class FlywayIntegrator {

    private final Logger log = LoggerFactory.getLogger(FlywayIntegrator.class);

    // inject datasource
    @Resource
    private javax.sql.DataSource dataSource;

    @PostConstruct
    private void onStartup() {

        if (dataSource == null) {
            log.error("no datasource found to execute the db migrations!");
            throw new EJBException(
                    "no datasource found to execute the db migrations!");
        }

        Flyway flyway = new Flyway();
        MigrationInfo migrationInfo = flyway.info().current();

        if (migrationInfo == null) {
            log.info("No existing database at the actual datasource");
        }
        else {
            log.info("Found a database with the version: {}", migrationInfo.getVersion() + " : "
                    + migrationInfo.getDescription());
        }

        flyway.setDataSource(dataSource);
        flyway.migrate();
        log.info("Successfully migrated to database version: {}", flyway.info().current().getVersion());
    }
}

The annotation @Singleton tells the container to instantiate the class as an EJB-Singleton which means that there is only one instance of this class. The problem with @Singleton is that the EJB container can define when to create it. The additional @Startup annotation tells the container to create it before any other invocation of the application is triggered. The @TransactionManagement annotation is necessary because the default behaviour of an EJB is to let the container manage transactions via JTA (Java Transaction API). This is not possible in this use case because Flyway internally manages its own transactions therefore the transaction management is delegated to the bean for this class.
In the lines 9 and 10 the datasource is injected via the @Resource annotation. This just works with the default datasource. If you are in a scenario where you have multiple datasources defined you can inject the datasource by name and switch the datasource name dynamically by using the maven resources plugin (as described here). Just insert a placeholder instead of the real datasource name for this to work.

@Resource(name = "${datasource.name}")
private DataSource dataSource;

The @PostConstruct annotation is part of the EJB lifecycle and is triggered after the dependency injection is done. The rest is similar to the previous solution. The datasource is set in Flyway and the migration is called.

The charm of this solution is that there are no dependencies to a special ORM or application server and that there is no internal API used. When everything is done via standard Java EE APIs you achieve high portability and stability. This demonstration source code can be checked out on my GitHub account in the project flywayIntegrations/flywayEjbIntegration.

 

5 thoughts on “Integrating Flyway with Java EE and using its datasource

  1. Thanks for this enlightening article.I tried @Resource(name = “${datasource.name}”)
    and got the following error in wildfly. How can I solve the problem.
    “WFLYCTL0412: Required services that are not installed:” => [“jboss.naming.context.java.module.\”Unduire-1.0-SNAPSHOT\”.\”Unduire-1.0-SNAPSHOT\”.env.\”${datasource.name}\””],
    “WFLYCTL0180: Services with missing/unavailable dependencies” => [“jboss.naming.context.java.module.\”Unduire-1.0-SNAPSHOT\”.\”Unduire-1.0-SNAPSHOT\”.env.\”com.karau.unduire.migration.FlywayIntegrator\”.dataSource is missing [jboss.naming.context.java.module.\”Unduire-1.0-SNAPSHOT\”.\”Unduire-1.0-SNAPSHOT\”.env.\”${datasource.name}\”]”]

  2. Hi Kim,

    when you use the placeholder ${datasource.name} you have to replace it at buildtime for example via maven like I described here. If you don’t want to do that you need to replace it manually with your concrete datasources name.

  3. Hi Seb,
    first of all, great article! I did not have a clue that this was possible at all, using wildfly. Could you please elaborate on the location of the migration files? Where should they be placed, if they are just plain sql scripts? Your example does not have any fallback scenarios, like do not continue startup if migrate failed… I don’t think this solution is viable for production without such additions. What do you think?

  4. Hi Geekrumper,
    as flyway works by default with a convention over configuration approach you have to put the migration scripts in ‘src/main/resources/db/migration’ (you can see a more complete example in this sample application I created https://github.com/areyouready/pantry).
    When a migration fails at startup time it throws an exception and the deployment is aborted. You don’t have to do anything special here.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.