Code First Migrations with Spring Boot, Hibernate and Liquibase

Michael Ushakov
4 min readNov 5, 2020

--

Today we are going to learn to how to create and manage Code First migrations for a Spring Boot application. But first, a little bit of theory.

Code First is an approach that allows us to create a database structure from model classes and configuration. It is quite convenient but somewhat suboptimal compared to Database First approach.

Migration is a mechanism that enables us to incrementally apply database structure changes.

Code First Migration is a migration generated based on changes you made to your code, be it persistence models or configuration.

Unfortunately, Hibernate does not have a capability to create and manage migrations, it can only update a whole database at once. And that is why we are going to need Liquibase.

If you work with Spring you probably already know how to configure Hibernate to update database automatically, but in case you don’t, all you got to do is, add these lines to your application.yml file:

spring:
jpa:
# other JPA settings
hibernate:
ddl-auto: update
# other spring settings

Before we continue, please make sure you have Gradle and Postgres because we are going to need the former to build the application and the latter to test our Code First Migration.

Now that we are all set, let us take a look at our build script.

Here is the dependency list:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compile 'org.postgresql:postgresql:42.2.1'
compile 'org.hibernate:hibernate-core:5.4.10.Final'
liquibaseRuntime 'org.liquibase:liquibase-core:3.8.1'
liquibaseRuntime "jakarta.xml.bind:jakarta.xml.bind-api:2.3.2"
liquibaseRuntime 'org.springframework.boot:spring-boot:2.2.1.RELEASE'
liquibaseRuntime 'org.postgresql:postgresql:42.2.9'
liquibaseRuntime 'org.springframework.data:spring-data-jpa:2.2.1.RELEASE'
liquibaseRuntime 'org.hibernate:hibernate-core:5.4.10.Final'
liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.8'
liquibaseRuntime 'ch.qos.logback:logback-core:1.2.3'
liquibaseRuntime 'ch.qos.logback:logback-classic:1.2.3'
liquibaseRuntime sourceSets.main.output
}

And this is Liquibase configuration that consists of three activities:

liquibase {
activities {
main {
changeLogFile "$projectDir/src/main/resources/db/changelog/changelog.xml"
outputFile outputLog
driver "org.postgresql.Driver"
url "jdbc:postgresql://localhost:5432/webportal"
username "developer"
password "123"
referenceUrl "hibernate:spring:com.wissance.webportal.application.model.entities?dialect=org.hibernate.dialect.PostgreSQL94Dialect&hibernate.physical_naming_strategy=com.wissance.utils.SnakePhysicalNamingStrategy"
referenceDriver 'liquibase.ext.hibernate.database.connection.HibernateDriver'
}
changesGen {
changeLogFile "$projectDir/src/main/resources/db/changelog/newChangelog.xml"
outputFile outputLog
driver "org.postgresql.Driver"
url "jdbc:postgresql://localhost:5432/webportal"
username "developer"
password "123"
referenceUrl "hibernate:spring:com.wissance.webportal.application.model.entities?dialect=org.hibernate.dialect.PostgreSQL94Dialect&hibernate.physical_naming_strategy=com.wissance.utils.SnakePhysicalNamingStrategy"
referenceDriver 'liquibase.ext.hibernate.database.connection.HibernateDriver'
}
// changesApply is used for Apply Migrations on Database
changesApply {
changeLogFile "$projectDir/src/main/resources/db/changelog/changelog.xml"
outputFile outputLog
driver "org.postgresql.Driver"
url "jdbc:postgresql://localhost:5432/monitor"
username "developer"
password "123"
referenceUrl "hibernate:spring:com.wissance.webportal.application.model.entities?dialect=org.hibernate.dialect.PostgreSQL94Dialect&hibernate.physical_naming_strategy=com.wissance.utils.SnakePhysicalNamingStrategy"
referenceDriver 'liquibase.ext.hibernate.database.connection.HibernateDriver'
}
}
runList = project.ext.runList
}

changesGen activity is used for migration generation based on difference between model classes and a database. Here we have to use a fake changelog (newChangelog.xml) otherwise activity fails with an error.

changesApply is used to apply migrations to a database via update command.

main activity is normally used when we run Spring application.

What you see inside activity bodies are Liquibase properties. Most of them are self-explanatory, except maybe one — referenceUrl. Let us take a closer look at it.

referenceUrl “hibernate:spring:com.wissance.webportal.application.model.entities?dialect=org.hibernate.dialect.PostgreSQL94Dialect&hibernate.physical_naming_strategy=com.wissance.utils.SnakePhysicalNamingStrategy”

The thing is, Liquibase Hibernate Extension allows us to pass a path to a package containing persistence models instead of a connection url.

I think you are already familiar with the parameters that follow the question mark, but I am going to tell you what they are anyway.

dialect stands for Hibernate dialect that specifies a database type and a version, so Hibernate can generate correct SQL.

physical_naming_strategy defines how Hibernate maps logical names to physical ones.

Let me elaborate. As you might notice Hibernate does not directly map a class or field name to a table or column name. Instead, it maps a class or column name to so-called logical name and then maps the logical name to a physical one. To be clear, physical name means a name of a table or a column.

Java Class Name -> Logical Name -> Physical Name (Table Name)

Java Class Field Name -> Logical Name -> Physical Name (Table Column Name)

So, there are two mappings and for each one Hibernate provides a naming strategy. Class(Column)-Name-to-Logical-Name is called Implicit Naming Strategy and Logical-Name-to-Table(Column)-Name is called Physical Naming Strategy.

There are some options for both strategies, but we decided to omit Implicit Implicit Naming Strategy (we are going to use the default one) and create our own Physical Naming Strategy called SnakePhysicalNamingStrategy which you can find here: https://github.com/Wissance/SpringUu/blob/master/src/com/wissance/utils/SnakePhysicalNamingStrategy.java

Now that we are through with the Gradle script, let us create our first Code First Migration. To generate the migration manually follow the instructions below:

.\gradlew.bat diffChangeLog updatesql -PrunList=’changesGen’

Copy the output file “diffOutputLog.txt” to changelog directory (optionally change the name and/or the extension)

Include it in changelog.xml like this:

<?xml version="1.0" encoding="UTF-8"?>    <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsdhttp://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">        <include file="2020-11-05_00-00-00_Migration_1_Init_data.sql" relativeToChangelogFile="true"/></databaseChangeLog>

Alternatively, you can use my PowerShell script that will take the whole process off your hands: https://github.com/Wissance/SpringUu/blob/master/tools/generateMigration.ps1. Put it in the same directory as build.gradle. It accepts two arguments: a migration number and a name.

.\generateMigration.ps1 1 Init

In case you were wondering, whitespaces in a migration name are allowed.

.\generateMigration.ps1 1 ‘Init data’

The script generates a migration with a name based on timestamp and command line parameters and moves it to changelog directory.

{timestamp}_Migration_1_Init_data.sql.

Congrats, you have just generated your very first migration with Hibernate and Liquibase!

Migrations can be applied either automatically on application startup (make sure liquibase is on in your application.yml) or manually using liquibase activity changesApply:

.\gradlew.bat update -PrunList=’changesApply’

Please subscribe If you would like to read more about our interesting experience with Spring Boot, also visit our GitHub (https://github.com/Wissance/SpringUu) and star some repositories if you’ll find it helpful or submit an issue to make our solutions better. It motivates us to make cross platform and cross build tools for migration generation and other purposes.

--

--

Michael Ushakov

I am a scientist (physicist), an engineer (hardware & software) and the CEO of Wissance (wissance.com)