How versioning maintenance happens in Liquibase

Introduction

In this tutorial I am going to show you how to work with different changes or versioning maintenance on database scripts without touching the original SQL scripts. So we will only work with the changes we need at a later point of time for our database.

Let’s say you have created a table through SQL script using liquibase framework and you forgot to add a column in the table. Later you thought that you need another column which is required to process your business requirements. So what next you are going to do, will you edit the original SQL script or will you create new SQL script only with your changes to append to the table?

This is the situation where liquibase comes to play to rescue you. So you don’t need to edit the original file. You just need to create another SQL file with your changes and you are done. Liquibase will do rest of the things for you.

Let’s say in this example, we are going to create a category table under roytuts database in MySQL server using SQL script through liquibase framework.

The structure of the table is given below:

CREATE TABLE `category` (
  `category_id` int unsigned NOT NULL AUTO_INCREMENT,
  `category_name` varchar(50) NOT NULL,
  `category_link` varchar(255) NOT NULL,
  `parent_id` int unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`category_id`),
  UNIQUE KEY `unique` (`category_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

So we have everything to represent a particular category.

Now let’s say after few days we get a requirement for sorting the categories when we are fetching from the database table. But we don’t want to sort categories by ascending or descending order, rather we want to have a column that will indicate the sort order of the categories.

Now we don’t need to edit the above table structure in the SQL script but we will create another SQL script to append our change to the existing table. The change we want is given below:

ALTER TABLE `category`
ADD `sort_order` int NOT NULL DEFAULT '0';

Now we will see how implement this scenario using Spring and Liquibase framework.

Prerequisites

Eclipse 2020-06, Java at least 1.8, Gradle 6.5.1, Maven 3.6.3, Spring Boot 2.3.1, MySQL 8.0.17, Liquibase 3.10.1

Project Setup

You can create either gradle or maven based project in Eclipse. The name of the project is spring-liquibase-version-maintenance.

If you are using gradle based project then you can create build.gradle script similar to below:

buildscript {
	ext {
		springBootVersion = '2.3.1.RELEASE'
	}
	
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version "${springBootVersion}"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
	implementation "org.springframework.boot:spring-boot-starter:${springBootVersion}"
    implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
    implementation("org.liquibase:liquibase-core:3.10.1")
    runtime("mysql:mysql-connector-java:8.0.17")
	
	//required for jdk 9 or above
	runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
}

If you are creating maven based project then you can use below pom.xml file:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-liquibase-version-maintenance</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.1.RELEASE</version>
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		
		<!--required only if jdk 9 or higher version is used-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>
	</dependencies>

    <build>
        <plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>at least 8</source>
					<target>at least 8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Application Properties

Create file src/main/resource/application.properties to load the database changelog file during Spring Boot application startup. We also declare the database settings in this file.

We don’t need to create any bean for database management and Liquibase configuration if we have the exact settings as shown below:

spring.datasource.url=jdbc:mysql://localhost/roytuts
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.liquibase.changeLog=classpath:liquibase/db.changelog-master.yml

The default location of changelog master file is classpath:/liquibase/changelog and Liquibase searches for a file db.changelog-master.yml. Therefore as we are using different location so we required to declare the location of the changelog master file.

In Spring Boot application the key liquibase.change-log does not work, so you need to use spring.liquibase.changeLog.

Here is the db.changelog-master.yml file:

databaseChangeLog:
  - changeSet:
      id: create_table
      author: Soumitra Roy
      changes:
      - sqlFile:
          dbms: mysql
          encoding: utf8
          endDelimiter: \n/\s*\n|\n/\s*$
          path: changelog/scripts/create-category-table-schema.sql
          relativeToChangelogFile: true
          splitStatements: true
          stripComments: true

Now create below SQL file create-category-table-schema.sql under src/main/resources/liquibase/changelog/scripts to create tables in the MySQL database:

CREATE TABLE `category` (
  `category_id` int unsigned NOT NULL AUTO_INCREMENT,
  `category_name` varchar(50) NOT NULL,
  `category_link` varchar(255) NOT NULL,
  `parent_id` int unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`category_id`),
  UNIQUE KEY `unique` (`category_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/

Create Main Class

Create below main class in order to start up the application and creation of the above table during the application startup.

package spring.liquibase.version.maintenance;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringLiquibaseVersionApp implements CommandLineRunner {

	public static void main(String[] args) {
		SpringApplication.run(SpringLiquibaseVersionApp.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		System.out.println("table successfully created");
	}

}

Testing the Application

Running the above main class you will find output in the console:

37.206  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOGLOCK
37.282  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE roytuts.DATABASECHANGELOGLOCK (ID INT NOT NULL, `LOCKED` BIT(1) NOT NULL, LOCKGRANTED datetime NULL, LOCKEDBY VARCHAR(255) NULL, CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))
37.349  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOGLOCK
37.371  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : DELETE FROM roytuts.DATABASECHANGELOGLOCK
37.375  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : INSERT INTO roytuts.DATABASECHANGELOGLOCK (ID, `LOCKED`) VALUES (1, 0)
37.387  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT `LOCKED` FROM roytuts.DATABASECHANGELOGLOCK WHERE ID=1
37.407  INFO 15344 --- [           main] l.lockservice.StandardLockService        : Successfully acquired change log lock
37.953  INFO 15344 --- [           main] l.c.StandardChangeLogHistoryService      : Creating database history table with name: roytuts.DATABASECHANGELOG
37.957  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE roytuts.DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED datetime NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35) NULL, `DESCRIPTION` VARCHAR(255) NULL, COMMENTS VARCHAR(255) NULL, TAG VARCHAR(255) NULL, LIQUIBASE VARCHAR(20) NULL, CONTEXTS VARCHAR(255) NULL, LABELS VARCHAR(255) NULL, DEPLOYMENT_ID VARCHAR(10) NULL)
38.005  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOG
38.011  INFO 15344 --- [           main] l.c.StandardChangeLogHistoryService      : Reading from roytuts.DATABASECHANGELOG
38.013  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT * FROM roytuts.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
38.017  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOGLOCK
38.081  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE `category` (
  `category_id` int unsigned NOT NULL AUTO_INCREMENT,
  `category_name` varchar(50) NOT NULL,
  `category_link` varchar(255) NOT NULL,
  `parent_id` int unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`category_id`),
  UNIQUE KEY `unique` (`category_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
38.158  INFO 15344 --- [           main] liquibase.changelog.ChangeSet            : SQL in file changelog/scripts/create-category-table-schema.sql executed
38.159  INFO 15344 --- [           main] liquibase.changelog.ChangeSet            : ChangeSet classpath:liquibase/db.changelog-master.yml::create_table::Soumitra Roy ran successfully in 104ms
38.162  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT MAX(ORDEREXECUTED) FROM roytuts.DATABASECHANGELOG
38.166  INFO 15344 --- [           main] liquibase.executor.jvm.JdbcExecutor      : INSERT INTO roytuts.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('create_table', 'Soumitra Roy', 'classpath:liquibase/db.changelog-master.yml', NOW(), 1, '8:59850f7fc0106f6a915f9921f69370e5', 'sqlFile', '', 'EXECUTED', NULL, NULL, '3.10.1', '4122818025')
38.184  INFO 15344 --- [           main] l.lockservice.StandardLockService        : Successfully released change log lock
38.435  INFO 15344 --- [           main] s.l.v.m.SpringLiquibaseVersionApp        : Started SpringLiquibaseVersionApp in 12.3 seconds (JVM running for 13.602)
table successfully created

In the log you can see that apart from your own table category there are two more tables get created under roytuts database as shown below in the image:

versioning maintenance in liquibase

You will also find one row inserted into the table DATABASECHANGELOG. This row identifies all details about the executed SQL script.

You will also find one row inserted into the table DATABASECHANGELOGLOCK. This row identifies whether current operation holds lock on changesets or not.

Alter Table

Now we will see how to alter the existing category table without editing the original create table schema SQL script.

So for this to happen we will create another SQL script which will contain the alter table statement.

Create a file called alter-category-table-schema.sql under the same path as we created for creating table:

ALTER TABLE `category`
ADD `sort_order` int NOT NULL DEFAULT '0';
/

Now you need to add another entry for this alter table operation into db.changelog-master.yml file:

...
- changeSet:
     id: alter_table
     author: Soumitra Roy
     changes:
     - sqlFile:
         dbms: mysql
         encoding: utf8
         endDelimiter: \n/\s*\n|\n/\s*$
         path: changelog/scripts/alter-category-table-schema.sql
         relativeToChangelogFile: true
         splitStatements: true
         stripComments: true

We also change the code snippets in main class to have updated message:

@Override
public void run(String... args) throws Exception {
	System.out.println("table successfully altered");
}

Re-testing the Application

Now running the main class again you will get below output:

38.842  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOGLOCK
38.859  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOGLOCK
38.866  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT `LOCKED` FROM roytuts.DATABASECHANGELOGLOCK WHERE ID=1
38.899  INFO 12728 --- [           main] l.lockservice.StandardLockService        : Successfully acquired change log lock
39.813  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT MD5SUM FROM roytuts.DATABASECHANGELOG WHERE MD5SUM IS NOT NULL LIMIT 1
39.817  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM roytuts.DATABASECHANGELOG
40.094  INFO 12728 --- [           main] l.c.StandardChangeLogHistoryService      : Reading from roytuts.DATABASECHANGELOG
40.095  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT * FROM roytuts.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
40.144  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : ALTER TABLE `category`
ADD `sort_order` int NOT NULL DEFAULT '0';
40.183  INFO 12728 --- [           main] liquibase.changelog.ChangeSet            : SQL in file changelog/scripts/alter-category-table-schema.sql executed
40.184  INFO 12728 --- [           main] liquibase.changelog.ChangeSet            : ChangeSet classpath:liquibase/db.changelog-master.yml::alter_table::Soumitra Roy ran successfully in 58ms
40.187  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT MAX(ORDEREXECUTED) FROM roytuts.DATABASECHANGELOG
40.192  INFO 12728 --- [           main] liquibase.executor.jvm.JdbcExecutor      : INSERT INTO roytuts.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('alter_table', 'Soumitra Roy', 'classpath:liquibase/db.changelog-master.yml', NOW(), 2, '8:831f648d261a82f879344307d427f20a', 'sqlFile', '', 'EXECUTED', NULL, NULL, '3.10.1', '4123720100')
40.211  INFO 12728 --- [           main] l.lockservice.StandardLockService        : Successfully released change log lock
40.419  INFO 12728 --- [           main] s.l.v.m.SpringLiquibaseVersionApp        : Started SpringLiquibaseVersionApp in 11.686 seconds (JVM running for 12.797)
table successfully altered

From the above output you will see this time only alter table script has run.

You can check the category table in the database and you will find that sort_order column has been added into it.

You will also notice that databasechangelog table is having one more entry for this alter script.

You can download both resultant SQL scripts from the source code below.

Source Code

Download

Thanks for reading.

Leave a Comment