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 Create, Read, Update, and Delete functionalities (CRUD operation).
Continue if you’ve gone through the environment setup and initial coding. If not, click on the link below😊
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.
- Version Control – Maintaining a record of your database schema modifications for convenience and historical precision.
- Consistency Across Environments – Make sure that the schema of your database is the same in the development, testing, and production environments.
- Automated Changes – Reduce the possibility of human error by automating the process of deploying database schema updates.
Core Concepts:
- 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.
- 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.
- 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