Java Spring Boot with Liquibase


This is a complete tutorial for beginners on developing a Java project using Spring Boot, Spring Data JPA, PostgreSQL, and Liquibase from scratch. This tutorial will help you understand basic concepts, configurations, tools required, and coding to execute a simple API application with CreateReadUpdate, and Delete functionalities (CRUD operation).

Continue if you’ve gone through the environment setup and initial coding. If not, click on the link below😊

Java Spring Boot with PostgreSQL — CRUD API Project: Part 1/2 (Tools Installation)

We will utilize Liquibase to manage our application’s database structure and maintain it in sync with the entities that make up our system. This tool is fantastic since it can manage the schema in a wide range of languages. For instance, we can use the XML, YAML, JSON, and SQL formats to define and restructure the database.

  1. Version Control – Maintaining a record of your database schema modifications for convenience and historical precision.
  2. Consistency Across Environments – Make sure that the schema of your database is the same in the development, testing, and production environments.
  3. Automated Changes – Reduce the possibility of human error by automating the process of deploying database schema updates.

Core Concepts:

  1. ChangeSet: A changeSet is a set of changes that need to be applied to a database. Liquibase tracks the execution of changes at a ChangeSet level.
  2. Change: A Change represents a single, atomic database modification. Whether it’s creating a table or altering a column, Liquibase abstracts these changes into easy-to-apply constructs.
  3. Changelog: The Changelog is a file containing multiple ChangeSets. It serves as a roadmap for your database’s evolution over time.

1. Open employer project in the VS Code

2. Add the following Liquibase dependency in the pom.xml

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

3. Add the following entry in the application.properties

spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml

db.changelog-master.yaml will act as our changelog file including all the changeSets.

Alternative:

Entry can be added to the pom.xml file or to a new file liquibase.properties in the src/main/resources to separate liquibase-related properties.

4. Create folders db\changelog under resources folder (green-highlighted)

5. Create a file db.changelog-master.yaml under the resources\db\changelog folder

databaseChangeLog:
  - include:
      file: classpath:/db/changelog/changes/001-initial-schema.sql

Liquibase supports other formats such as XML, YAML, JSON, and SQL formats. Use whichever format you prefer for the development.

In this blog, we are using YAML format.

6. Create a folder changes under the resources\db\changelog folder and a new file 001-initial-schema.sql

Download 001-initial-schema.sql from the source code URL mentioned at the bottom of the blog.

Liquibase supports other formats such as XML, YAML, JSON, and SQL formats. Use whichever format you prefer for the development.

In this blog, we are using SQL format.

The above SQL file depicts the creation of the employer table as per the employer.java entity file.

7. Final structure

8. Run the project in VS Code

9. Open DBeaver to verify

Liquibase creates two tables databasechangelog and databasechangeloglock when it runs in a database for the first time.

It uses the databasechangelog table to keep track of the status of the execution of changeSets and databasechangeloglock to prevent concurrent executions of Liquibase.

10. Open Postman

Add a new employer

Authorization — Type → No Auth

Headers — Content-Type → application/json

11. Now let’s add a new column to the employer table and verify the Liquibase changelog functionality

Update Employer.java to add the employerAge column

package com.example.employer.Entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "employer")
public class Employer {
    
    @Id
    @Column(name = "employer_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int employerID;

    @Column(name = "employer_name", length = 50)
    private String employerName;

    @Column(name = "employer_email", length = 60)
    private String employerEmail;

    @Column(name = "employer_age")
    private int employerAge;

    public Employer(int employerID, String employerName, String employerEmail, int employerAge){
        this.employerID = employerID;
        this.employerName = employerName;
        this.employerEmail = employerEmail;
        this.employerAge = employerAge;
    }

    public Employer(String employerName, String employerEmail, int employerAge){
        this.employerName = employerName;
        this.employerEmail = employerEmail;
        this.employerAge = employerAge;
    }

    public Employer(){
    }

    public int getEmployerID() {
        return employerID;
    }

    public void setEmployerID(int employerID) {
        this.employerID = employerID;
    }

    public String getEmployerName() {
        return employerName;
    }

    public void setEmployerName(String employerName) {
        this.employerName = employerName;
    }

    public String getEmployerEmail() {
        return employerEmail;
    }

    public void setEmployerEmail(String employerEmail) {
        this.employerEmail = employerEmail;
    }

    public int getEmployerAge() {
        return employerAge;
    }

    public void setEmployerAge(int employerAge) {
        this.employerAge = employerAge;
    }

    @Override
    public String toString() {
        return "{" +
            " employerID='" + getEmployerID() + "'" +
            ", employerName='" + getEmployerName() + "'" +
            ", employerEmail='" + getEmployerEmail() + "'" +
            ", employerAge='" + getEmployerAge() + "'" +
            "}";
    }
}

Update EmployerDTO.java to add the employerAge column

package com.example.employer.DTO;

public class EmployerDTO {
    
    private int employerID;
    private String employerName;
    private String employerEmail;
    private int employerAge;

    public EmployerDTO(int employerID, String employerName, String employerEmail, int employerAge) {
        this.employerID = employerID;
        this.employerName = employerName;
        this.employerEmail = employerEmail;
        this.employerAge = employerAge;
    }

    public EmployerDTO() {
    }

    public int getEmployerID() {
        return employerID;
    }

    public void setEmployerID(int employerID) {
        this.employerID = employerID;
    }

    public String getEmployerName() {
        return employerName;
    }

    public void setEmployerName(String employerName) {
        this.employerName = employerName;
    }

    public String getEmployerEmail() {
        return employerEmail;
    }

    public void setEmployerEmail(String employerEmail) {
        this.employerEmail = employerEmail;
    }

    public int getEmployerAge() {
        return employerAge;
    }

    public void setEmployerAge(int employerAge) {
        this.employerAge = employerAge;
    }

    @Override
    public String toString() {
        return "{" +
            " employerID='" + getEmployerID() + "'" +
            ", employerName='" + getEmployerName() + "'" +
            ", employerEmail='" + getEmployerEmail() + "'" +
            ", employerAge='" + getEmployerAge() + "'" +
            "}";
    }
}

Update EmployerSaveDTO.java to add the employerAge column

package com.example.employer.DTO;

public class EmployerSaveDTO {
    
    private String employerName;
    private String employerEmail;
    private int employerAge;

    public EmployerSaveDTO(String employerName, String employerEmail, int employerAge) {
        this.employerName = employerName;
        this.employerEmail = employerEmail;
        this.employerAge = employerAge;
    }

    public EmployerSaveDTO() {
    }

    public String getEmployerName() {
        return employerName;
    }

    public void setEmployerName(String employerName) {
        this.employerName = employerName;
    }

    public String getEmployerEmail() {
        return employerEmail;
    }

    public void setEmployerEmail(String employerEmail) {
        this.employerEmail = employerEmail;
    }

    public int getEmployerAge() {
        return employerAge;
    }

    public void setEmployerAge(int employerAge) {
        this.employerAge = employerAge;
    }

    @Override
    public String toString() {
        return "{" +
            " employerName='" + getEmployerName() + "'" +
            ", employerEmail='" + getEmployerEmail() + "'" +
            ", employerAge='" + getEmployerAge() + "'" +
            "}";
    }
}

Update EmployerUpdateDTO.java to add the employerAge column

package com.example.employer.DTO;

public class EmployerUpdateDTO {
    
    private int employerID;
    private String employerName;
    private String employerEmail;
    private int employerAge;

    public EmployerUpdateDTO(int employerID, String employerName, String employerEmail, int employerAge) {
        this.employerID = employerID;
        this.employerName = employerName;
        this.employerEmail = employerEmail;
        this.employerAge = employerAge;
    }

    public EmployerUpdateDTO() {
    }

    public int getEmployerID() {
        return employerID;
    }

    public void setEmployerID(int employerID) {
        this.employerID = employerID;
    }

    public String getEmployerName() {
        return employerName;
    }

    public void setEmployerName(String employerName) {
        this.employerName = employerName;
    }

    public String getEmployerEmail() {
        return employerEmail;
    }

    public void setEmployerEmail(String employerEmail) {
        this.employerEmail = employerEmail;
    }

    public int getEmployerAge() {
        return employerAge;
    }

    public void setEmployerAge(int employerAge) {
        this.employerAge = employerAge;
    }

    @Override
    public String toString() {
        return "{" +
            " employerID='" + getEmployerID() + "'" +
            ", employerName='" + getEmployerName() + "'" +
            ", employerEmail='" + getEmployerEmail() + "'" +
            ", employerAge='" + getEmployerAge() + "'" +
            "}";
    }
}

Update EmployerServiceImpl.java to add the employerAge column

package com.example.employer.ServiceImpl;

import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.employer.DTO.EmployerDTO;
import com.example.employer.DTO.EmployerSaveDTO;
import com.example.employer.DTO.EmployerUpdateDTO;
import com.example.employer.Entity.Employer;
import com.example.employer.Repo.EmployerRepo;
import com.example.employer.Service.EmployerService;

@Service
public class EmployerServiceImpl implements EmployerService {

    @Autowired
    private EmployerRepo employerRepo;

    @Override
    public List<EmployerDTO> getAllEmployers() {
        List<Employer> employerList = employerRepo.findAll();

        List<EmployerDTO> employerDTOList = new ArrayList<>();
        for(Employer employer : employerList){
            EmployerDTO employerDTO = new EmployerDTO(employer.getEmployerID(), employer.getEmployerName(), employer.getEmployerEmail(), employer.getEmployerAge());
            employerDTOList.add(employerDTO);
        }

        return employerDTOList;
    }

    @Override
    public String addEmployer(EmployerSaveDTO employerSaveDTO) {
        
        Employer employer = new Employer(employerSaveDTO.getEmployerName(), employerSaveDTO.getEmployerEmail(), employerSaveDTO.getEmployerAge());
        employerRepo.save(employer);
        return employer.getEmployerName();
    }

    @Override
    public String updateEmployer(EmployerUpdateDTO employerUpdateDTO) {
        if(employerRepo.existsById(employerUpdateDTO.getEmployerID())){
            Employer employer = employerRepo.getById(employerUpdateDTO.getEmployerID());

            employer.setEmployerName(employerUpdateDTO.getEmployerName());
            employer.setEmployerEmail(employerUpdateDTO.getEmployerEmail());
            employer.setEmployerAge(employerUpdateDTO.getEmployerAge());

            employerRepo.save(employer);
            return "Updated Employer ID: " + employerUpdateDTO.getEmployerID();
        }else{
            return "Employer ID not present";
        }
    }

    @Override
    public String deleteEmployer(int employerID) {
        if(employerRepo.existsById(employerID)){
            employerRepo.deleteById(employerID);

            return "Employer ID deleted: " + employerID;
        }else{
            return "Employer ID not present";
        }
    }
}

12. Create a new file 002-schema.sql under the resources\db\changelog\changes folder to include the new column addition to the employer table

ALTER TABLE employer
ADD COLUMN IF NOT EXISTS employer_age INT;

13. Update db.changelog-master.yaml file to include the new changeSet

databaseChangeLog:
  - include:
      file: classpath:/db/changelog/changes/001-initial-schema.sql
      file: classpath:/db/changelog/changes/002-schema.sql

14. Run the project in VS Code

15. Open Postman

Add a new employer

Authorization — Type → No Auth

Headers — Content-Type → application/json

16. Open DBeaver to verify

When we run our Spring Boot application, Liquibase will automatically detect and apply the changes defined in the Changelog files. We could see that Liquibase managed to apply the schema defined in the 002-schema.sql to our database.

Once the changesets are applied, normally we cannot edit the same ones further. It’s best to make a new changeset always. That’s how Liquibase ensures the consistency of our database versioning.


Thus, we come to the end of the project using Spring Boot, PostgreSQL, and Liquibase😊

Thank you for reading! I hope you’re able to understand and proceed with the development of this explanation and if so, please share to help others too. 😊

Well, there are a lot of areas for improvement in this project and, for any new suggestions or comments, I’m all ears.

Download project from: https://github.com/sharuroy16/employer-liquibase

Leave a Comment

Your email address will not be published. Required fields are marked *