Search code examples
javaguicederbydropwizard

Where is the correct place to initialize an embedded database in a DropWizard application?


I am building a DropWizard based application that will have an embedded Derby database.

Where in the Dropwizard framework would be the appropriate place to test if the database exists and if not create it.

Right now I am configuring the database in the DataSourceFactory in the .yml file that is provided by the dropwizard-db module and that is not available until the run() method is called.

I am using Guice as well in this application, so solutions involving Guice will be accepted as well.

Is there an earlier more appropriate place to test for and create the database?


Solution

  • as asked, I am going to provide my solution. Backstory, I am using guicey (https://github.com/xvik/dropwizard-guicey) which in my humble opinion is a fantastic framework. I use that for integrating with guice, however I expect most implementations will be similar and can be adopted. In addition to this, I also use liquibase for database checking and consistency.

    Firstly, during initialisation, I am creating a bundle that does my verification for me. This bundle is a guicey concept. It will automatically be run during guice initialisation. This bundle looks like this:

    /**
     * Verifying all changelog files separately before application startup.
     * 
     * Will log roll forward and roll back SQL if needed 
     * 
     * @author artur
     *
     */
    public class DBChangelogVerifier extends ComparableGuiceyBundle {
    
        private static final String ID = "BUNDLEID";
    
        private static final Logger log = Logger.getLogger(DBChangelogVerifier.class);
    
        private List<LiquibaseConfiguration> configs = new ArrayList<>();
    
        public void addConfig(LiquibaseConfiguration configuration) {
            this.configs.add(configuration);
        }
    
    
        /**
         * Attempts to verify all changelog definitions with the provided datasource
         * @param ds
         */
        public void verify(DataSource ds) {
            boolean throwException = false;
            Contexts contexts = new Contexts("");
            for(LiquibaseConfiguration c : configs) {
                try(Connection con = ds.getConnection()) {
                        Database db = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(con));
                        db.setDatabaseChangeLogLockTableName(c.changeLogLockTableName());
                        db.setDatabaseChangeLogTableName(c.changeLogTableName());
                        Liquibase liquibase = new ShureviewNonCreationLiquibase(c.liquibaseResource(), new ClassLoaderResourceAccessor(), db);
                        liquibase.getLog();
                        liquibase.validate();
                        List<ChangeSet> listUnrunChangeSets = liquibase.listUnrunChangeSets(contexts, new LabelExpression());
    
                        if(!listUnrunChangeSets.isEmpty()) {
                            StringWriter writer = new StringWriter();
                            liquibase.update(contexts, writer);
                            liquibase.futureRollbackSQL(writer);
                            log.warn(writer.toString());
                            throwException = true;
                        }
                } catch (SQLException | LiquibaseException e) {
                    throw new RuntimeException("Failed to verify database.", e);
                }
            }
    
            if(throwException){
                throw new RuntimeException("Unrun changesets in chengelog.");
            }
        }
    
        /**
         * Using init to process and validate to avoid starting the application in case of errors. 
         */
        @Override
        public void initialize(GuiceyBootstrap bootstrap) {
            Configuration configuration = bootstrap.configuration();
            if(configuration instanceof DatasourceConfiguration ) {
                DatasourceConfiguration dsConf = (DatasourceConfiguration) configuration;
                ManagedDataSource ds = dsConf.getDatasourceFactory().build(bootstrap.environment().metrics(), "MyDataSource");
                verify(ds);
            }
        }
    
        @Override
        public String getId() {
            return ID;
        }
    
    }
    

    Note that ComparableGuiceBundle is an interface I added so I can have an order in the bundles and their init functions.

    This bundle will automatically be initialised by guicey and the init method will be called, supplying me with a datasource. In the init (the same thread) I am calling verify. This means, that if verification fails, the startup of my application fails and it will refuse to finish starting.

    In my startup code, I simply add this bundle to the Guicey configuration so that Guice can be aware of it:

    // add all bundles to the bundles variable including the Liquibase bundle. 
    // registers guice with dropwizard
            bootstrap.addBundle(GuiceBundle.<EngineConfigurationImpl>builder()
                    .enableAutoConfig("my.package")
                    .searchCommands(true)    
                    .bundles(bundles.toArray( new GuiceyBundle[0]))
                    .modules(getConfigurationModule(), new CoreModule())
                                       .build()
            );
    

    That is all I need to do. Guicey takes care of the rest. During application startup it will initialise all bundles passed to it. Due to it being comparable, the bundle verifying my database is the first one and will be executed first. Only if that bundle successfully starts up will the other ones start up as well.

    For the liquibase part:

    public void verify(DataSource ds) {
            boolean throwException = false;
            Contexts contexts = new Contexts("");
            for(LiquibaseConfiguration c : configs) {
                try(Connection con = ds.getConnection()) {
                        Database db = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(con));
                        db.setDatabaseChangeLogLockTableName(c.changeLogLockTableName());
                        db.setDatabaseChangeLogTableName(c.changeLogTableName());
                        Liquibase liquibase = new ShureviewNonCreationLiquibase(c.liquibaseResource(), new ClassLoaderResourceAccessor(), db);
                        liquibase.getLog();
                        liquibase.validate();
                        List<ChangeSet> listUnrunChangeSets = liquibase.listUnrunChangeSets(contexts, new LabelExpression());
    
                        if(!listUnrunChangeSets.isEmpty()) {
                            StringWriter writer = new StringWriter();
                            liquibase.update(contexts, writer);
                            liquibase.futureRollbackSQL(writer);
                            log.warn(writer.toString());
                            throwException = true;
                        }
                } catch (SQLException | LiquibaseException e) {
                    throw new RuntimeException("Failed to verify database.", e);
                }
            }
    
            if(throwException){
                throw new RuntimeException("Unrun changesets in chengelog.");
            }
        }
    

    As you can see from my setup, I can have multiple changelog configurations that can be checked. In my startup code I look them up and add them to the bundle.

    Liquibase will choose the correct database for you. If no database is available it will error. If the connection isn't up, it will error.

    If it finds unran changesets, it will print out roll forward and rollback SQL. If the md5sum isn't correct, it will print that. In any case, if the database isn't consistent with the changesets, it will refuse to start up.

    Now in my case, I do not want liquibase to create anything. It is a pure validation process. However liquibase does give you the option to run all changesets, create tables etc. You can read about it in the docs. It's fairly straight forward.

    This approach pretty much integrates liquibase with a normal startup, rather than using the database commands with dropwizard to execute them manually.

    I hope that helps, let me know if you have any questions.

    Artur