1

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.

Antonio
  • 51
  • 6
  • We have some documentation for both Liquibase and Hibernate and Liquibase and Spring Boot that might help: * [Using Liquibase with Hibernate](https://docs.liquibase.com/install/tutorials/hibernate.html) * [Using Liquibase with Spring Boot](https://docs.liquibase.com/tools-integrations/springboot/springboot.html) If you've already resolved this, please let us know what the solution was! – tabbyfoo May 10 '22 at 17:11
  • Thanks for the reply. I created a Context refreshed listener (called when Spring finishes startup) with the following method (added in answer to this post) This looks to be working as expected, please let me know if there is something that looks like it can be improved. – Antonio May 11 '22 at 16:26

1 Answers1

1

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

Antonio
  • 51
  • 6