Configuring Liquibase in a Spring Boot project with Gradle

Edward Heaver
6 min readNov 13, 2022

Starting a new project is always interesting. There are so many decisions to make! What build tool should you use? Do you need a database? How should you architect the project?

For some, this is a burden and something that they don’t want to deal with. I personally love making these decisions and exploring the pros and cons of each. For me, tools like JHipster take the fun out of this process somewhat: being very opinionated on how we should work with our tools. Don’t get me wrong; I do enjoy the batteries-included approach when I just want to get stuff done. But, at heart, I’m a tinkerer. So doing deep dives into how the tools work and tuning them to work in the way that I want is enjoyable for me. So, rather than reach for the off-the-shelf project starter in JHipster, I decided to roll my own starter with Gradle, Liquibase, and Spring Boot to better learn how this part of my stack works.

Getting set up

The obvious first place to start is the ‘getting started’ section in the Liquibase docs:

The documentation here is fairly clear on what we need to do to set up the plugin. While I knew some Gradle already, I could already see this was going to be much more involved than simply adding the plugin to the closure. Liquibase needs to know what the working code is when a task runs, so I add the dependencies to liquibaseRuntime so that the Gradle tasks can pick up the necessary code.

When comparing the code from the docs and the snippet here, there are major differences that aren’t very clearly explained:

  1. The Gradle plugin has no understanding of my code. Gradle does, but the plugin is essentially its own context. So, I add sourceSets.main.output to the plugin’s runtime.
  2. The Gradle plugin has a transitive dependency on Picocli that isn’t installed by default when adding the plugin or Liquibase. This is clarified in the GitHub Readme but doesn’t feature in the docs at all.
  3. If running the plugin with Spring Boot and Hibernate, it will not understand what Hibernate is. Luckily, I can simply add Hibernate to the runtime. I also need to include the JPA for the same reason.

Finding the information that allowed me to set this up was tricky. There wasn’t much information that was recent or relevant. But, it is out there. Next, I configure the plugin.

The above is the configuration that the plugin expects. This is largely based on the CLI Liquibase has, so is very easy to change and manipulate as needed (as I’ll cover shortly). Some key things to note:

  1. The driver needs to be set to the database driver being used. This also needs to be in the runtime as mentioned.
  2. The classpath should point to the root of the project (or the root project if running multiple). This is so that the runtime and plugin know what the full classpath is for my application
  3. The changelog file should be the fully qualified path from the root of the project. If I want to produce YAML, XML, or even Groovy changelogs, I just need to make the file on the path have that extension.
  4. The referenceUrl is optional but is needed to generate the migrations using diff-change-log. If that isn’t present, the Gradle task will not know what to diff against.

The way the diff-change-log command works is by using the referenceUrl to connect to the database and analysing the schema. It then uses this schema to diff against the database’s schema at the URL in the configuration. We can supply these arguments to the CLI if installed for the same effect.

The only required piece of the referenceURL is:

hibernate:spring:com.org.choosemysnooze?dialect=org.hibernate.dialect.PostgreSQLDialect

This gets the schema that Hibernate generates for entities in the app that is used when the spring.jpa.hibernate.ddl-auto property is set to anything other than none and generates the relevant SQL in the correct dialect. The other pieces in the configuration are used to generate the correct names of the tables from the entities. This can be safely left off, but makes creating the database changesets much easier.

Making it work

Now that the plugin bits are set up, there are a few more things to do to make sure everything works correctly. By default, the Gradle tasks from the plugin will always default to the configuration set up in the Liquibase closure. Of course, that may not always be the config we want. To change what the plugin uses for specific tasks, I created another task that updates the config. Luckily, any properties added in this way will overwrite the existing values from the configuration.

By using the dependsOnfunction on the task, we can set the order we want tasks to run. This makes it simple to update the configuration with the location of the root changelog to what we need for the specific task, in this case, the update task. We also call the same function on the rollbackCounttask.

When calling the rollbackCounttask, we also need to supply the LiquibaseCommandValue argument as a project property.

$ gradle rollbackCount -PliquibaseCommandValue=1

We could set this as a project property in the build.gradle (we do this for the changeset name), but it’s my preference to always supply it.

final DEFAULT_ROLLBACK_COUNT = 1;

The final thing to do to make this work is to make sure that our app compiles our code before running the task diffChangeLog and apply the plugin.

apply plugin: "org.liquibase.gradle"

diffChangeLog.dependsOn compileJava

Using the plugin tasks

Now that we’ve done all the configuration, the build.gradle file should look like this:

The final piece of the puzzle is the MIGR_NAME constant. This is created and used for the changelog name when using the diffChangeLog task. I do this by using a custom function to create a timestamp in my build script and concatenating it to a string:

def getTimestamp() {
def date = new Date()
return date.format('yyyyMMddHHmmss')
}
final MIGR_NAME = "src/main/resources/db/changelogs/changelog_" + getTimestamp() + ".yaml"

The path of this is a folder in the resources directory of the src folder and is saved as YAML. As mentioned before, Liquibase is smart enough to save the changeset in the correct file format based on the file name. Once this gets created, I include the newly created changeset in the root file. Luckily, Liquibase gives us a handy helper in the form of includeAll

databaseChangeLog:
- includeAll:
path: "src/main/resources/db/changelogs"

This will include all the changesets from this folder regardless of the format they are saved in. So, if I decided to start doing changesets in XML, I could without issue.

With all the setup done, I can use the tasks:

  • update — to apply all changesets included in the root-changelog.yamlfile
$ gradle update # applies the changesets referenced in the root-changelog
  • diffChangeLog — to create a changelog based on the current state of my entities and the database
$ gradle diffChangeLog # creates a new changeset file
  • rollbackCount — to rollback a given number of changesets
$ gradle rollbackCount -PliquibaseCommandValue=1 # rollsback one changeset

For my purposes, this is all I would need. But, if I needed to implement more, it’d be simple. The plugin code very closely mirrors the CLI Liquibase offer, so it would be a matter of setting up the configuration with a custom task.

Review

Now that this is done and working I have a much higher appreciation for the work the maintainers of JHipster do. Doing the grunt work to get this functionality working was definitely fun, but frustrating at the same time. To review, the process of setting up the Gradle plugin was:

  1. Adding it to build.gradle (obviously)
  2. Adding the main activity configuration as per the docs, with some addendums that aren’t very clearly mentioned
  3. Setting up some custom Gradle tasks to merge configuration for specific tasks as needed.

While seemingly a very simple process, the Devil is in the detail here. I’m looking forward to using set up more and hopefully making this into a proper starter project once I’ve optimised how everything works.

--

--

Edward Heaver
Edward Heaver

Written by Edward Heaver

Backend Engineer with a passion for clean code and fun features

No responses yet