Search code examples
liquibase

Using Liquibase to populate data with ORM


I am currently using Liquibase to create the database schema. It's a spring boot application that uses hibernate ORM.

Liquibase is changing the database schema at the beginning of application startup, before ORM is initialized (so that ORM looks at correct schema when being initialized).

However after the application is started I want to run Liquibase for the second time to populate data in the database with the help of the ORM and java code ( using java changeset: Java code changeset in liquibase ).

The reason to use Liquibase to populate the data with ORM and java is because Liquibase works correctly in a cluster (ex. if we have 3 nodes started at the same time only one node will populate the data).

Is is possible to run Liquibase in "2 phases"?

First time, during the application startup to update the database schema, and second time, after application was started, to populate some data using ORM. We need to do that from java code.

Previously when using Flyway we did that by having 2 Flyway instances, each with individual changes, and running the first instance at the beginning of the application startup before ORM is initialized and the second instance after the startup is complete and ORM is initialized.


Solution

  • Following solution seems to be working:

    @Component
    class LiquibaseOrmMigrationRunner(val dataSource: DataSource, val resourceLoader: ResourceLoader, val environment: Environment) :
        ApplicationListener<ContextRefreshedEvent> {
    
        override fun onApplicationEvent(event: ContextRefreshedEvent) {
            val isLiquibaseEnabled = environment.getProperty("spring.liquibase.enabled", Boolean::class.java, true)
    
            if (!isLiquibaseEnabled) {
                return
            }
    
            var liquibase: Liquibase? = null
    
            try {
                val changeLogFile = "classpath:/db/changelog/orm/db.changelog-master-orm.yaml"
                val connection = dataSource.connection
                val liquibaseConnection = JdbcConnection(connection)
                val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(liquibaseConnection)
                database.databaseChangeLogTableName = "ORMDATABASECHANGELOG"
                database.databaseChangeLogLockTableName = "ORMDATABASECHANGELOGLOCK"
                val resourceAccessor = SpringResourceAccessor(resourceLoader)
                liquibase = Liquibase(changeLogFile, resourceAccessor, database)
                liquibase.update(Contexts(), LabelExpression())
            } catch (e: SQLException) {
                throw DatabaseException(e)
            } finally {
                if (liquibase != null) {
                    liquibase.close()
                }
            }
        }
    }
    

    This creates an additional liquibase instance that runs after the default one configured by spring boot.

    To populate the database using orm the additional liquibase instance executes the following changeset

    databaseChangeLog:
        - changeSet:
              id: initial_data
              author: author
              changes:
                  -   customChange: { "class": "com.example.LiquibaseInitialDataMigrationOrm" }
    
    

    The custom change looks like this:

    class LiquibaseInitialDataMigrationOrm : CustomTaskChange {
    
        override fun execute(database: Database?) {
            val initialDataService = SpringApplicationContextSingleton.getBean("initialDataService") as InitialDataService
    
            initialDataService.populateInitialData()
        }
    
        override fun getConfirmationMessage(): String {
            return "Initial Data Liquibase ORM Migration finished"
        }
    
        @Suppress("EmptyFunctionBlock")
        override fun setUp() {
        }
    
        @Suppress("EmptyFunctionBlock")
        override fun setFileOpener(resourceAccessor: ResourceAccessor?) {
        }
    
        override fun validate(database: Database?): ValidationErrors {
            return ValidationErrors()
        }
    }
    
    

    The custom change uses a singleton that contains the spring application context