Databases are required in every application to store and index data. It becomes hassle if the database changes are not maintained well enough. As the application starts to grow database changes grow too. To maintain databases changes Liquibase is one tool to solve it.

NOTE: This post assumes that you have basic spring boot (v3.0) project setup with all the required dependencies and POSTGRESQl.

Liquibase Configuration for Spring boot

Add following dependency to pom.xml

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

Configure liquibase properties in application.yml or application.properties file

 spring:
   liquibase:
       change-log: classpath:changelog/main.xml
       enabled: true

change-log tells the spring that all the database migrations are in the src/main/resources/changelog/main.xml file

enabled flag if true will run the liquibase configuration when the spring starts

main.xml will contain all the database migrations.

Database Migration

Create file with name ‘1690137730-init-schema.xml’ under src/main/resources/changelog folder and include it in main.xml.
You can name however you want, but it’s good to append timestamp to the file name so it easier to track the migrations.

<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-latest.xsd">
    <include file="changelog/1690137730-init-schema.xml"/>    
</databaseChangeLog>

Below are the details of 1690137730-init-schema.xml. Here we create two tables country and state and then add foreign key of country in the state table.

Few things to consider

  • add unique changeSet id ( usually name of the file should suffice)
  • add name of the author
  • multiple changesets can be added into single file

<?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-latest.xsd">

    <changeSet id="1690137730-init-sequences" author="my-restaurant" dbms="postgresql">
        <createSequence sequenceName="country_country_id_seq" startValue="1" />
        <createSequence sequenceName="state_state_id_seq" startValue="1" />
    </changeSet>
    <changeSet id="1690137730-init-schema" author="dev">
        <createTable tableName="country">
            <column name="country_id" type="bigint" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="name" type="varchar2(100)">
                <constraints nullable="false"/>
            </column>            
            <column name="updated" type="timestamp" defaultValueComputed="CURRENT_TIMESTAMP"/>
            <column name="created" type="timestamp" defaultValueComputed="CURRENT_TIMESTAMP"/>
        </createTable>

        <createTable tableName="state">
            <column name="state_id" type="bigint" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="name" type="varchar2(100)">
                <constraints nullable="false"/>
            </column>
            <column name="code" type="varchar2(100)"/>            
            <column name="country_id" type="bigint">
                <constraints nullable="false"/>
            </column>
            <column name="updated" type="timestamp" defaultValueComputed="CURRENT_TIMESTAMP"/>
            <column name="created" type="timestamp" defaultValueComputed="CURRENT_TIMESTAMP"/>
        </createTable>
        <addForeignKeyConstraint baseTableName="state" baseColumnNames="country_id" constraintName="fK_state_country_id"
                                 referencedTableName="country"
                                 referencedColumnNames="country_id"/>        
    </changeSet>    
</databaseChangeLog>

Apply Migrations

Assuming that you have valid postgresql connection string in the spring.datasource.url.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demodb?currentSchema=myapp

Run the application, this will run the migrations for the above datasource.url connection string. After the application runs successfully sequences and tables should be created under demodb/myapp schema.

Liquibase also creates two extra tables databasechangelog and databasechangeloglock to keep track which migrations have been applied, so that on next turn it won’t apply the migrations again which have been applied.

Entity Reference

Create State and Country entities. These entities will be used by the repositories for spring JPA

@Table(name = "state")
@Entity
@Getter
@Setter
public class State {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "stateSeq")
    @SequenceGenerator(name = "stateSeq", sequenceName = "state_state_id_seq", allocationSize = 1)
    @Column(name = "state_id")
    private Long stateId;

    @Column(name = "name", nullable = false)
    @NotNull
    private String name;

    @Column(name = "code")
    private String code;    

    @ManyToOne
    @JoinColumn(name = "country_id")
    @NotNull
    private Country country;
    
    @Column(name = "created", updatable = false, columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    @CreatedDate
    private LocalDateTime created;

    @Column(name = "updated", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    @LastModifiedDate
    private LocalDateTime updated;

}
@Entity
@Table(name = "country")
@Getter
@Setter
public class Country {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "countrySeq")
    @SequenceGenerator(name = "countrySeq", sequenceName = "country_country_id_seq", allocationSize = 1)
    @Column(name = "country_id")
    private Long countryId;
    
    @Column(name = "name", nullable = false)
    @NotNull
    private String name;
        
    @OneToMany(mappedBy = "country")
    private List<State> states = new ArrayList<>();
    
    @Column(name = "created", updatable = false, columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    @CreatedDate
    private LocalDateTime created;

    @Column(name = "updated", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    @LastModifiedDate
    private LocalDateTime updated;
}

Update Migrations

To add another migration, create new file under the changelog folder and include that in the main.xml. Add your changes with new changeset and run the application. On successful start of application new migrations should be applied too.

Conclusion

In this post we saw how keep track of the database migrations using the liquibase tool and integration with spring boot.