DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify
  • How To Build Web Service Using Spring Boot 2.x
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps

Trending

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Event-Driven Pipelines With Apache Pulsar and Go
  • Slopsquatting: Building a Scanner That Catches AI-Hallucinated Packages Before They Reach Production
  • The Invisible OOMKill: Why Your Java Pod Keeps Restarting in Kubernetes
  1. DZone
  2. Data Engineering
  3. Databases
  4. Easy Database Migration With Liquibase

Easy Database Migration With Liquibase

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Apr. 28, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
21.0K Views

Join the DZone community and get the full member experience.

Join For Free

In this post, we take a look at how we can easily manage our database migration scripts by means of Liquibase. Liquibase will automatically execute necessary database migration scripts during application startup. We will explore some of the features of Liquibase by means of a simple Spring Boot application in combination with a PostgreSQL database.

1. Introduction

When your application makes use of a relational database like PostgreSQL, you inevitably will have to cope with database migration scripts. When your application evolves, new functionality is being added, and this often impacts your database schema. You probably have multiple database instances running for development, test, acceptance, and production. How will you keep track of which scripts have been applied to which instance? 

You can develop something yourself, which will keep track of an internal database schema version and apply the necessary database scripts based on this version, but you can also make use of a tool like Liquibase, which will provide this task for you.

We will create a simple Spring Boot application that makes use of a PostgreSQL database. Docker is a prerequisite for this post. Installation instructions for Docker are available at the Docker website.

The sources being used in this blog are available at GitHub. Beware that the GitHub repository contains the state of the sources as we will reach at the end of this post.

2. Create a Simple Application

Our sample application will have the possibility to add an employee and to retrieve the list of added employees by means of an HTTP request. We start at Spring Initialzr and select the following dependencies: Spring Web, PostgreSQL Driver, and Liquibase Migration. This leads to the following dependencies in our pom:

XML
xxxxxxxxxx
1
17
 
1
<dependency>
2
    <groupId>org.springframework.boot</groupId>
3
    <artifactId>spring-boot-starter-web</artifactId>
4
</dependency>
5
<dependency>
6
    <groupId>org.springframework.boot</groupId>
7
    <artifactId>spring-boot-starter-jdbc</artifactId>
8
</dependency>
9
<dependency>
10
    <groupId>org.liquibase</groupId>
11
    <artifactId>liquibase-core</artifactId>
12
</dependency>
13
<dependency>
14
    <groupId>org.postgresql</groupId>
15
    <artifactId>postgresql</artifactId>
16
    <scope>runtime</scope>
17
</dependency>


The domain object, Employee, contains just an id, a first name, and a last name. The getters and setters are left out for brevity.

Java
xxxxxxxxxx
1
11
 
1
public class Employee {
2
    private long id;
3
    private String firstName, lastName;
4
 
5
    public Employee(long id, String firstName, String lastName) {
6
        this.id = id;
7
        this.firstName = firstName;
8
        this.lastName = lastName;
9
    }
10
    // Getters and setters
11
}


We create a DAO (Data Access Object), which contains the methods for accessing our database. We use JdbcTemplate for our database access and add two methods: one for adding an employee and one for retrieving the list of employees.

Java
xxxxxxxxxx
1
26
 
1
@Repository
2
public class EmployeeDao {
3
 
4
    @Autowired
5
    JdbcTemplate jdbcTemplate;
6
 
7
    public List getAllEmployees() {
8
        return jdbcTemplate.query("SELECT * FROM EMPLOYEE", new EmployeeRowMapper());
9
    }
10
 
11
    public void addEmployee(final String firstName, String lastName) {
12
        jdbcTemplate.update("INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME) VALUES (?, ?)", firstName, lastName);
13
    }
14
 
15
    private static final class EmployeeRowMapper implements RowMapper {
16
 
17
        @Override
18
        public Employee mapRow(final ResultSet rs, final int rowNum) throws SQLException {
19
            return employee = new Employee(rs.getLong("ID"),
20
                                           rs.getString("FIRST_NAME"),
21
                                           rs.getString("LAST_NAME"));
22
        }
23
 
24
    }
25
 
26
}


The EmployeeController will take care of processing the HTTP requests in order to add and retrieve employees.

Java
xxxxxxxxxx
1
20
 
1
@Controller
2
public class EmployeeController {
3
 
4
    @Autowired
5
    public EmployeeDao employeeDao;
6
 
7
    @PostMapping(path = "/addEmployee")
8
    public @ResponseBody
9
    String addEmployee(@RequestParam String firstName, @RequestParam String lastName) {
10
        employeeDao.addEmployee(firstName, lastName);
11
        return "Saved Employee";
12
    }
13
 
14
    @GetMapping(path = "/getAllEmployees")
15
    public @ResponseBody
16
    Iterable getAllEmployees() {
17
        return employeeDao.getAllEmployees();
18
    }
19
 
20
}


Now, it is time to create our database scripts. By default, Spring expects the Liquibase database changelog file at the following location: src/main/resources/db/changelog/db.changelog-master.yaml. 

Liquibase expects your database changes in this changelog master file. Inside the file, your database changes must be indicated by different changesets, each with a unique id. The complete list of options can be found here; we will explore some of these options. The syntax is quite easy and recognizable. In our case, we just create the Employee table, the three columns, and add an auto-increment option to our id column.

YAML
xxxxxxxxxx
1
25
 
1
databaseChangeLog:
2
  - changeSet:
3
      id: 1
4
      author: gunter
5
      changes:
6
        - createTable:
7
            tableName: employee
8
            columns:
9
              - column:
10
                  name: id
11
                  type: serial
12
                  autoIncrement: true
13
                  constraints:
14
                    primaryKey: true
15
                    nullable: false
16
              - column:
17
                  name: first_name
18
                  type: varchar(255)
19
                  constraints:
20
                    nullable: false
21
              - column:
22
                  name: last_name
23
                  type: varchar(255)
24
                  constraints:
25
                    nullable: false


The only thing for us to do is to add the database properties to the application.properties file and to set up the database. The application.properties contains the datasource properties in order to be able to connect to the PostgreSQL database, which we will run locally.

Properties files
xxxxxxxxxx
1
 
1
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
2
spring.datasource.username=postgres
3
spring.datasource.password=root


We make use of the PostgreSQL Docker image to run the database:

Shell
xxxxxxxxxx
1
 
1
$ docker run -p 5432:5432 --name mypostgres -e POSTGRES_PASSWORD=root -d postgres


Start the Spring Boot application:

Shell
xxxxxxxxxx
1
 
1
$ mvn spring-boot:run


Let’s add an entry to the list of employees:

Shell
xxxxxxxxxx
1
 
1
$ curl --data "firstName=John" --data "lastName=Doe" http://localhost:8080/addEmployee
2
Saved Employee


Retrieve the list of employees:

Shell
xxxxxxxxxx
1
 
1
$ curl http://localhost:8080/getAllEmployees
2
[{"id":1,"firstName":"John","lastName":"Doe"}]


We now know that our application is up and running and the database table has been created.

3. Inspect the Database

As you probably noticed during the startup of the application, quite some Liquibase log statements passed by. It is also interesting to inspect the database a bit further in order to verify what has happened here. We can make use of the psql command inside the PostgreSQL container in order to do so.

Shell
xxxxxxxxxx
1
 
1
$ docker exec -it mypostgres psql -U postgres
2
psql (12.2 (Debian 12.2-2.pgdg100+1))
3
Type "help" for help.
4
 
5
postgres=#


Now that we have a Postgres command prompt, we can execute database commands. Let’s retrieve a list of tables with the \d command:

Shell
xxxxxxxxxx
1
 
1
postgres=# \d
2
                  List of relations
3
 Schema |         Name          |   Type   | Owner 
4
--------+-----------------------+----------+----------
5
 public | databasechangelog     | table    | postgres
6
 public | databasechangeloglock | table    | postgres
7
 public | employee              | table    | postgres
8
 public | employee_id_seq       | sequence | postgres
9
(4 rows)


As expected, we see our employee table and the corresponding sequence table. We also notice two tables databasechangelog and databasechangeloglock which are created by Liquibase itself. The databasechangelog table contains a list of all the changes that have been run against the database.

The databasechangeloglock table is used to make sure two machines don’t attempt to modify the database at the same time. Let’s inspect the databasechangelog table:

Shell
xxxxxxxxxx
1
16
 
1
postgres=# select * from databasechangelog ;
2
-[ RECORD 1 ]-+-------------------------------------------------
3
id            | 1
4
author        | gunter
5
filename      | classpath:/db/changelog/db.changelog-master.yaml
6
dateexecuted  | 2020-03-22 16:14:33.492585
7
orderexecuted | 1
8
exectype      | EXECUTED
9
md5sum        | 8:8d6c01228a9985939df0c3b0b1c4ae38
10
description   | createTable tableName=employee
11
comments      | 
12
tag           | 
13
liquibase     | 3.8.7
14
contexts      | 
15
labels        | 
16
deployment_id | 4890073466


The databasechangelog table contains one entry with the details of the changeset, just as we expected.

4. Use XML

By default, Spring Boot expects the changelog to be in YAML format. Personally, we prefer the XML format, which seems to be more readable, but it is mainly a matter of taste. First of all, we need to configure Spring Boot in order to use the XML format. We add the following line to the application.properties file:

Properties files
xxxxxxxxxx
1
 
1
spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml


Besides that, we will refer from our master changelog to version update files. This is a Liquibase best practice. For more best practices, see the Liquibase website. The db.changelog-master.xml file now looks like the following:

XML
xxxxxxxxxx
1
 
1
<databaseChangeLog
2
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
6
 
7
    <include file="/db/changelog/db.changelog-1.0.xml"/>
8
 
9
</databaseChangeLog>


In the resources/db/changelog/ directory, we add a file db.changelog-1.0.xml, which contains the same create table entries as in the YAML file we created earlier. We also add a rollback tag. When something goes wrong during the SQL execution, the rollback statement will be executed, leaving your database in a consistent state.

XML
xxxxxxxxxx
1
24
 
1
<databaseChangeLog
2
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
6
 
7
    <changeSet author="gunter" id="changelog-1.0">
8
        <createTable tableName="employee">
9
            <column name="id" type="serial" autoIncrement="true">
10
                <constraints nullable="false" primaryKey="true"/>
11
            </column>
12
            <column name="first_name" type="varchar(255)">
13
                <constraints nullable="false"/>
14
            </column>
15
            <column name="last_name" type="varchar(255)">
16
                <constraints nullable="false"/>
17
            </column>
18
        </createTable>
19
        <rollback>
20
            <dropTable tableName="employee"/>
21
        </rollback>
22
    </changeSet>
23
 
24
</databaseChangeLog>


Stop and remove the PostgreSQL container:

Shell
xxxxxxxxxx
1
 
1
$ docker stop mypostgres
2
$ docker rm mypostgres


Start the PostgreSQL container with docker run just like we did before and start the application with mvn spring-boot:run.

By means of the psql command, we check the contents of the databasechangelog table and notice that the filename has changed to the XML file.

Shell
xxxxxxxxxx
1
16
 
1
postgres=# select * from databasechangelog;
2
-[ RECORD 1 ]-+-----------------------------------
3
id            | changelog-1.0
4
author        | gunter
5
filename      | /db/changelog/db.changelog-1.0.xml
6
dateexecuted  | 2020-03-28 11:13:20.163418
7
orderexecuted | 1
8
exectype      | EXECUTED
9
md5sum        | 8:8d6c01228a9985939df0c3b0b1c4ae38
10
description   | createTable tableName=employee
11
comments      | 
12
tag           | 
13
liquibase     | 3.8.7
14
contexts      | 
15
labels        | 
16
deployment_id | 5390400140


5. Add a Migration Script

In the next section, we will add a new column country to the Employee table. We will need to change the Employee, EmployeeDao, and EmployeeController to support the new column.

The Employee is changed as follows:

Java
xxxxxxxxxx
1
12
 
1
public class Employee {
2
    private long id;
3
    private String firstName, lastName, country;
4
 
5
    public Employee(long id, String firstName, String lastName, String country) {
6
        this.id = id;
7
        this.firstName = firstName;
8
        this.lastName = lastName;
9
        this.country = country;
10
    }
11
    // Getters and setters
12
}


The EmployeeDao is changed as follows:

Java
xxxxxxxxxx
1
16
 
1
public void addEmployee(final String firstName, String lastName, String country) {
2
    jdbcTemplate.update("INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, COUNTRY) VALUES (?, ?, ?)",
3
            firstName, lastName, country);
4
}
5
 
6
private static final class EmployeeRowMapper implements RowMapper {
7
 
8
    @Override
9
    public Employee mapRow(final ResultSet rs, final int rowNum) throws SQLException {
10
        return new Employee(rs.getLong("ID"),
11
                            rs.getString("FIRST_NAME"),
12
                            rs.getString("LAST_NAME"),
13
                            rs.getString("COUNTRY"));
14
    }
15
 
16
}


The EmployeeController is changed as follows:

Java
xxxxxxxxxx
1
 
1
@PostMapping(path = "/addEmployee")
2
public @ResponseBody
3
String addEmployee(@RequestParam String firstName, @RequestParam String lastName, @RequestParam String country) {
4
    employeeDao.addEmployee(firstName, lastName, country);
5
    return "Saved Employee";
6
}


Add a db.changelog-2.0.xml file to the resources/db/changelog/ directory:

XML
xxxxxxxxxx
1
18
 
1
<databaseChangeLog
2
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
6
 
7
    <changeSet author="gunter" id="changelog-2.0">
8
        <addColumn tableName="employee">
9
            <column name="country" type="varchar(255)"/>
10
        </addColumn>
11
        <rollback>
12
            <dropColumn tableName="employee">
13
                <column name="country" type="varchar(255)"/>
14
            </dropColumn>
15
        </rollback>
16
    </changeSet>
17
 
18
</databaseChangeLog>


And add a line to the master file:

XML
xxxxxxxxxx
1
 
1
<include file="/db/changelog/db.changelog-2.0.xml"/>


Run the application; the log shows us clearly that the migration script is executed:

Shell
xxxxxxxxxx
1
 
1
2020-03-28 11:33:12.775 INFO 11117 --- [ main] liquibase.executor.jvm.JdbcExecutor : ALTER TABLE public.employee ADD country VARCHAR(255) 
2
2020-03-28 11:33:12.776 INFO 11117 --- [ main] liquibase.changelog.ChangeSet : Columns country(varchar(255)) added to employee 
3
2020-03-28 11:33:12.779 INFO 11117 --- [ main] liquibase.changelog.ChangeSet : ChangeSet /db/changelog/db.changelog-2.0.xml::changelog-2.0::gunter ran successfully in 4ms


Check the schema of the Employee table where we can verify that the country column is added:

Shell
xxxxxxxxxx
1
10
 
1
postgres=# \d employee
2
Table "public.employee"
3
   Column   |          Type          | Collation | Nullable | Default 
4
------------+------------------------+-----------+----------+----------------------------------
5
 id         | integer                |           | not null | generated by default as identity
6
 first_name | character varying(255) |           | not null | 
7
 last_name  | character varying(255) |           | not null | 
8
 country    | character varying(255) |           |          | 
9
Indexes:
10
"employee_pkey" PRIMARY KEY, btree (id)


Add an employee via the web request and retrieve all employees:

Shell
xxxxxxxxxx
1
 
1
$ curl --data "firstName=John" --data "lastName=Doe" --data "country=The Netherlands" http://localhost:8080/addEmployee
2
Saved Employee
3
$ curl http://localhost:8080/getAllEmployees
4
[{"id":1,"firstName":"John","lastName":"Doe","country":"The Netherlands"}]


6. Something About Contexts

The last feature of Liquibase we will discuss is contexts. A context can be added to a changeset. For example, when a changeset should only be executed on a test environment. First, we will create two Spring Profiles, one for prod, and one for test. We rename the application.properties file to application-prod.properties and add the following line to it:

Properties files
xxxxxxxxxx
1
 
1
spring.liquibase.contexts=prod


We create an application-test.properties, similar to the application-prod.properties and change the above line to context test.

We add a new changelog db.changelog-3.0.xml, which inserts a record in the Employee table and we add the context test to it:

XML
xxxxxxxxxx
1
15
 
1
<databaseChangeLog
2
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
6
 
7
    <changeSet author="gunter" id="changelog-3.0" context="test">
8
        <insert tableName="employee">
9
            <column name="first_name" value="Foo"/>
10
            <column name="last_name" value="Bar"/>
11
            <column name="country" value="Sweden"/>
12
        </insert>
13
    </changeSet>
14
 
15
</databaseChangeLog>


The other changelogs are given the prod or test contexts and will be executed for context prod as well as for context test.

Run the application again and run it with the prod Spring profile:

Shell
xxxxxxxxxx
1
 
1
$ mvn spring-boot:run -Dspring-boot.run.profiles=prod


Verify the Employee table or execute the getAllEmployees web request. We notice that it returns an empty list.

Stop the application and run it with the test Spring profile:

Shell
xxxxxxxxxx
1
 
1
$ mvn spring-boot:run -Dspring-boot.run.profiles=test


The console log shows us already that the 3.0 changelog is being executed:

Shell
xxxxxxxxxx
1
 
1
2020-03-29 11:22:23.806 INFO 9277 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO public.employee (first_name, last_name, country) VALUES ('Foo', 'Bar', 'Sweden')
2
2020-03-29 11:22:23.807 INFO 9277 --- [ main] liquibase.changelog.ChangeSet : New row inserted into employee
3
2020-03-29 11:22:23.815 INFO 9277 --- [ main] liquibase.changelog.ChangeSet : ChangeSet /db/changelog/db.changelog-3.0.xml::changelog-3.0::gunter ran successfully in 10ms


Execute the getAllEmployees web request and you will notice that the record from our changelog is returned.

Shell
x
 
1
$ curl http://localhost:8080/getAllEmployees
2
[{"id":1,"firstName":"Foo","lastName":"Bar","country":"Sweden"}]


7. Conclusion

In this post, we looked at how Liquibase can help you with versioning and migrating your database changes. It is easy to set up and easy to use. We only scratched the surface of what is possible with Liquibase, so take the time to take a look at all the other features it has to offer.

Database Relational database Liquibase Spring Framework application shell Spring Boot PostgreSQL XML

Published at DZone with permission of Gunter Rotsaert. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify
  • How To Build Web Service Using Spring Boot 2.x
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook