Database Migrations : Liquibase for Spring Boot Projects
Tags: java, springboot, liquibase, database
Published: 2024-05-26 13:01:21

There are 2 ways with which Liquibase can be used in the context of a Spring Boot Project to perform database migrations, the first one is during the development phase where each developer will want add his own modifications and the modifications added by his collegues to his local database, the second one is during the deployment phase where we will want to gather all the modifications and run them against a database used by a released version of the project

In this post we will cover how Liquibase can be integrated with a Spring Boot project to help use perfrom database migration both phases, and we will be using examples from the repository that you can check at : [Github Repository](https://github.com/Aharmaz/liquibase-demo)

**Fundamental Concepts of Liquibase** 

*Changeset and Migration File* :

A changeset is the smallest coutable unit of change that Liquibase can perfrom and register on a target database and it can contain one or more operations.

A migration file is a file responsible for hosting one or more changesets.

In Liquibase executing a changeset does not necessarly mean executing the migration file that contains that changeset, because it may contain other changesets that were not executed

*Changelog* : 

A changelog is a file containing a list of ordered changesets, or references to migration files containing changesets

*DATABASECHANGELOG Table* : 

This is the table Liquibase creates on the target database and uses to keep track of what changesets have already been applied 

A changeset won't be executed a second time if it has already been executed and registered in the DATABASECHANGELOG table

Each change set is identified in Liquibase by 3 properties, id, author, and filepath. When Liquibase executes a changeset it calculates a checksum for its content and stores it inside the DATABASECHANGELOG table with the 3 properties in order to make sure that a changeset has not been changed over time

If Liquibase notices that a changeset has been modified after it has been applied using the checksum calculations it will throw an error or a warning

**Common Configuration** 

The first thing to do is to add the migration scripts containing the changesets intended to be applied on the target database : 

- V1__creating_schema.sql
- V2__add_category_column_to_books_table.sql
- V3__adding_authors_table.sql

Then we need to add a changelog file referencing the migration scripts following a specific order 

```
<?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"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
    <include file="db/migrations/V1__creating_schema.sql" />
    <include file="db/migrations/V2__add_category_column_to_books_table.sql" />
    <include file="db/migrations/V3__adding_authors_table.sql" />
</databaseChangeLog>
```

**Configuring Liquibase to run migrations on application startup**

Most of the time this behavior of running migrations on the application startup is used locally (when the spring boot application is executed with the local profile), this is why we should add information about the database and the location of the changelog file in the local properties file (application-local.properties in the example)

```
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demo_liquibase
    username: postgres
    password: changemeinproduction
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: none
  liquibase:
    change-log: classpath:/db/migrations/changelog.xml
```

The next step is to add the dependency of liquibase in the pom.xml file of the project (build.gradle file if you are using Gradle)

```
<dependency>
  <groupId>org.liquibase</groupId>
  <artifactId>liquibase-core</artifactId>
</dependency>
```

When starting the application, Spring Boot will notice the presence of the liquibase dependency on the runtime classpath and will trigger the autoconfiguration classes related to liquibase, and an automatic migration process is going to be started against the configured database, here is an example of logging that we should get when starting the app : 

```
2024-05-26T13:04:49.188+01:00  INFO 16644 --- [           main] liquibase.database                       : Set default schema name to public
2024-05-26T13:04:49.372+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Creating database history table with name: public.databasechangelog
2024-05-26T13:04:49.418+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Reading from public.databasechangelog
2024-05-26T13:04:49.476+01:00  INFO 16644 --- [           main] liquibase.lockservice                    : Successfully acquired change log lock
2024-05-26T13:04:49.478+01:00  INFO 16644 --- [           main] liquibase.command                        : Using deploymentId: 6725089478
2024-05-26T13:04:49.480+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Reading from public.databasechangelog
Running Changeset: db/migrations/V1__creating_schema.sql::1::aymane
2024-05-26T13:04:49.507+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Custom SQL executed
2024-05-26T13:04:49.510+01:00  INFO 16644 --- [           main] liquibase.changelog                      : ChangeSet db/migrations/V1__creating_schema.sql::1::aymane ran successfully in 18ms
Running Changeset: db/migrations/V2__add_category_column_to_books_table.sql::1::aymane
2024-05-26T13:04:49.523+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Custom SQL executed
2024-05-26T13:04:49.525+01:00  INFO 16644 --- [           main] liquibase.changelog                      : ChangeSet db/migrations/V2__add_category_column_to_books_table.sql::1::aymane ran successfully in 5ms
Running Changeset: db/migrations/V3__adding_authors_table.sql::1::aymane
2024-05-26T13:04:49.540+01:00  INFO 16644 --- [           main] liquibase.changelog                      : Custom SQL executed
2024-05-26T13:04:49.542+01:00  INFO 16644 --- [           main] liquibase.changelog                      : ChangeSet db/migrations/V3__adding_authors_table.sql::1::aymane ran successfully in 12ms
2024-05-26T13:04:49.547+01:00  INFO 16644 --- [           main] liquibase.util                           : UPDATE SUMMARY
2024-05-26T13:04:49.547+01:00  INFO 16644 --- [           main] liquibase.util                           : Run:                          3
2024-05-26T13:04:49.547+01:00  INFO 16644 --- [           main] liquibase.util                           : Previously run:               0
2024-05-26T13:04:49.547+01:00  INFO 16644 --- [           main] liquibase.util                           : Filtered out:                 0
2024-05-26T13:04:49.548+01:00  INFO 16644 --- [           main] liquibase.util                           : -------------------------------
2024-05-26T13:04:49.548+01:00  INFO 16644 --- [           main] liquibase.util                           : Total change sets:            3
2024-05-26T13:04:49.548+01:00  INFO 16644 --- [           main] liquibase.util                           : Update summary generated
2024-05-26T13:04:49.549+01:00  INFO 16644 --- [           main] liquibase.command                        : Update command completed successfully.
Liquibase: Update has been successful. Rows affected: 3
2024-05-26T13:04:49.555+01:00  INFO 16644 --- [           main] liquibase.lockservice                    : Successfully released change log lock
2024-05-26T13:04:49.557+01:00  INFO 16644 --- [           main] liquibase.command                        : Command execution complete
```

**Configuring Liquibase to run migrations independently from running the application**

This behavior is used at the deployment phase when we will want to grab all the migration scripts added since the last release and execute them against database deployed on dev, staging or production environment

For that There is a Maven plugin for liquibase that can be used, but before adding it we should add a configuration file liquibase.yml to contain information about the target database and the location of the changelog file 

```
url: jdbc:postgresql://localhost:5432/demo_liquibase
username: postgres
password: changemeinproduction
driver: org.postgresql.Driver
changeLogFile: src/main/resources/db/migrations/changelog.xml
```

Then we should add the plugin to the pom file in the build section :

```
<build>
        <plugins>
            <plugin>
                <groupId>org.liquibase</groupId>
                <artifactId>liquibase-maven-plugin</artifactId>
                <version>4.5.0</version>
                <configuration>
                    <propertyFile>
                        src/main/resources/liquibase.yml
                    </propertyFile>
                </configuration>
            </plugin>
        </plugins>
</build>
```
An important thing is to remember to disable triggering the liquibase migration process on application startup when starting the application with a profile like integration or production (not local profile), here is an example of the application-prod.yml file : 

```
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demo_liquibase
    username: postgres
    password: changemeinproduction
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: none
  liquibase:
    enabled: false
```

Finally we can use the following command to trigger the migration process : 

```
./mvnw liquibase:update
```

**Conclusion**

Using Liquibase with Spring Boot offers a robust solution for managing database changes in a controlled and efficient manner. It enables developers to focus on delivering features without worrying about the complexities of database migrations, making it an essential tool for any Spring Boot-based project.