Streamlining Database Changes with Liquibase: A Spring Boot Integration Guide
Modern applications often require frequent database schema updates. Keeping these updates synchronized across environments (development, staging, production) can be challenging. This is where Liquibase, a database version control tool, shines.
In this article, we’ll explore how Liquibase helps manage database changes efficiently and demonstrate its power using a realistic industry example with an H2 database and Spring Boot.
What is Liquibase?
Liquibase is an open-source tool for database schema management. It allows developers to:
- Track database changes in version control systems.
- Apply updates consistently across environments.
- Rollback changes when needed.
- Automate database migrations as part of CI/CD pipelines.
The Problem It Solves
Without a tool like Liquibase, database updates can lead to:
- Inconsistencies: Different environments having different schema versions.
- Manual Errors: Developers making mistakes while applying SQL scripts manually.
- Rollback Complexity: Difficulty reverting changes during failures.
Liquibase solves these issues by using declarative change logs (XML, YAML, JSON, or SQL) that describe schema changes in a controlled and auditable way.
Practical Industry Use Case
— — — — — — — — — — — — — — — — — — — — -
Problem Statement
A company needs to build a user management system with the following requirements:
- Initially, create a table to store user information.
- Later, add a column for
phone_number
to theusers
table. - Add a
roles
table to manage user roles, along with a relationship table (user_roles
). - Demonstrate a rollback scenario where a change is reverted.
We’ll use Liquibase to manage these schema changes step by step.
Setting Up Liquibase with Spring Boot and H2
Step 1: Add Dependencies
Add the required dependencies in your build.gradle
file:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
implementation 'org.liquibase:liquibase-core'
}
Step 2: Configure the Application
Update the application.yml
file:
spring:
datasource:
url: jdbc:h2:file:./data/testdb
username: sa
password:
driver-class-name: org.h2.Driver
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
h2:
console:
enabled: true
path: /h2-console
- Persistent Database: Using
jdbc:h2:file:./data/testdb
ensures that the database persists even after the server stops. - H2 Console: Access the H2 database console at
http://localhost:8080/h2-console
.
Step 3: First Iteration (Create Users Table)
Create a master changelog (db.changelog-master.xml
) to include individual changesets:
<?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/changelog/changeset-1-create-users.xml" />
</databaseChangeLog>
Define the first changeset to create the users
table:
<?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">
<changeSet id="1" author="codefarm">
<createTable tableName="users">
<column name="id" type="int" autoIncrement="true">
<constraints primaryKey="true" />
</column>
<column name="username" type="varchar(50)">
<constraints nullable="false" unique="true" />
</column>
<column name="email" type="varchar(100)">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
</databaseChangeLog>
Restart the application to create the users
table.
Verify: Access the H2 console and run:
SELECT * FROM users;
Step 4: Second Iteration (Add Phone Number)
Update the master changelog:
<include file="db/changelog/changeset-2-add-phone.xml" />
Define the second changeset to add the phone_number
column:
<?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">
<changeSet id="2" author="codefarm">
<addColumn tableName="users">
<column name="phone_number" type="varchar(15)">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
Restart the application to apply the change.
Verify: Run:
DESCRIBE users;
Step 5: Third Iteration (Add Roles Table)
Update the master changelog:
<include file="db/changelog/changeset-3-add-roles.xml" />
Define the third changeset to create the roles
and user_roles
tables:
<?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">
<changeSet id="3" author="codefarm">
<createTable tableName="roles">
<column name="id" type="int" autoIncrement="true">
<constraints primaryKey="true" />
</column>
<column name="role_name" type="varchar(50)">
<constraints nullable="false" unique="true" />
</column>
</createTable>
<createTable tableName="user_roles">
<column name="user_id" type="int">
<constraints nullable="false" />
</column>
<column name="role_id" type="int">
<constraints nullable="false" />
</column>
<addForeignKeyConstraint baseTableName="user_roles" baseColumnNames="user_id"
referencedTableName="users" referencedColumnNames="id" />
<addForeignKeyConstraint baseTableName="user_roles" baseColumnNames="role_id"
referencedTableName="roles" referencedColumnNames="id" />
</createTable>
</changeSet>
</databaseChangeLog>
Restart the application to apply the changes.
Verify: Run:
SELECT * FROM roles;
SELECT * FROM user_roles;
Step 6: Rollback Scenario
Imagine the phone_number
column is no longer required. Liquibase allows you to rollback this change.
Rollback Configuration
Update the second changeset to include rollback instructions:
<?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">
<changeSet id="2" author="codefarm">
<addColumn tableName="users">
<column name="phone_number" type="varchar(15)">
<constraints nullable="true" />
</column>
</addColumn>
<rollback>
<dropColumn tableName="users" columnName="phone_number" />
</rollback>
</changeSet>
</databaseChangeLog>
Executing Rollback
Run the rollback command with Gradle:
./gradlew liquibaseRollback -PliquibaseCommandValue=1
Restart the application and verify:
DESCRIBE users;
The phone_number
column will be removed.
Benefits of Liquibase
- Version Control: Tracks schema changes in a structured manner.
- Rollback Support: Reverts changes during failures (e.g.,
liquibase rollbackCount 1
). - Consistency Across Environments: Ensures all environments have the same schema.
- CI/CD Integration: Automates schema migrations in pipelines.
Conclusion
Liquibase is a powerful tool for managing database changes efficiently. By following this guide, you’ve learned how to:
- Set up Liquibase with Spring Boot and H2.
- Manage schema updates through multiple iterations.
- Rollback changes seamlessly.
- Maintain a consistent and version-controlled database schema.
If you found this useful clap the article and follow me for more article.
Here is the video on Youtube channel #codefarm to demo this which you should checkout —