In today’s world of microservices, each microservice has its own set of information stored in some kind of data store. This data store can be either a database or some other system that can retain the data for a period of time. We moved to microservices because we didn’t want one set of functionality not to be impacted by the changes in other functionality.
After this migration to microservices then comes the question of how can we track the changes in microservices like code changes and database schema changes. There are a couple of solutions available in the market to track these kinds of changes. Here, in this article, we will look at one of the solutions to track the database schema changes using Liquibase in a micronaut service.
Create a new micronaut application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch. The previous command creates a Micronaut application with the default package com.knoldus.micronaut
in a directory named micronaut-with-liquibase
.
mn create-app
com.knoldus.micronaut.micronaut-with-liquibase \
--features=data-jdbc,postgres,liquibase \
--build=maven \
--lang=java \
Create new Entity
Create a new database entity to save the employee
package com.knoldus.micronaut.entity;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Version;
import javax.validation.constraints.NotBlank;
@MappedEntity //Map the class to the table defined in the schema
public class Employee {
@Id //Specifies the ID of an entity
@GeneratedValue(value = GeneratedValue.Type.AUTO)
private Long id;
@Version //To enable optimistic locking for your entity
private Long version;
@NonNull
@NotBlank
private String name;
@Nullable
private int age;
public Employee(@NonNull String name, @Nullable int age) {
this.name = name;
this.age = age;
}
public Long getId() {
return id;
}
public Long getVersion() {
return version;
}
@NonNull
public String getName() {
return name;
}
@Nullable
public int getAge() {
return age;
}
public void setVersion(Long version) {
this.version = version;
}
}
Configure Liquibase
Configure the database migrations directory for Liquibase in application.yml
.
src/main/resources/application.yml
liquibase:
enabled: true
datasources:
default:
change-log: 'classpath:db/liquibase-changelog.xml'
Create the following files with the database schema creation:
src/main/resources/db/liquibase-changelog.xml
<?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-3.1.xsd">
<include file="changelog/create-employee.xml"
relativeToChangelogFile="true"/>
</databaseChangeLog>
src/main/resources/db/changelog/create-employee.xml
<?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-3.1.xsd">
<changeSet id="01" author="vimalk">
<createTable tableName="employee"
remarks="A table to contain employee information">
<column name="id" type="BIGINT">
<constraints nullable="false"
unique="true"
primaryKey="true"
primaryKeyName="employeePK"/>
</column>
<column name="version" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="age" type="INT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
Run the application, the SQL file is run by Liquibase, which also produces the application’s necessary schema.
The database schema has below three tables:
databasechangelog
databasechangeloglock
- employee
The tables databasechangelog
and databasechangeloglock
are used by Liquibase to keep track of database migrations. The employee table will look like this:
After the changeset, the employee
table looks like:
Column | Nullable |
---|---|
id | NO |
version | NO |
name | NO |
age | NO |
Make changes in the schema
We will make the age attribute nullable.
src/main/java/com/knoldus/micronaut/entity/Employee.java
@Nullable
private final Integer age;
public Employee(@NonNull String name,
@Nullable Integer age) {
this.name = name;
this.age = age;
}
@Nullable
public Integer getAge() {
return age;
}
Add a new changeset to drop the null constraint. Update the liquibase-changelog.xml as per below snippet
src/main/resources/db/liquibase-changelog.xml
<?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-3.1.xsd">
<include file="changelog/create-employee.xml"
relativeToChangelogFile="true"/>
<include file="changelog/nullable-age.xml"
relativeToChangelogFile="true"/>
</databaseChangeLog>
src/main/resources/db/changelog/nullable-age.xml
<?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-3.1.xsd">
<changeSet id="02" author="vimalk">
<dropNotNullConstraint tableName="employee"
columnName="age"/>
</changeSet>
</databaseChangeLog>
After the changeset, the employee
table looks like:
Column | Nullable |
---|---|
id | NO |
version | NO |
name | NO |
age | YES |
Enable Liquibase endpoint
To enable the Liquibase endpoint, add the management
dependency on your classpath.
pom.xml
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
<scope>compile</scope>
</dependency>
src/main/resources/application.yml
endpoints:
liquibase:
enabled: true
sensitive: false
Go to endpoint http://localhost:8080/liquibase, below response can be expected
[
{
"name": "default",
"changeSets": [
{
"id": "01",
"tag": null,
"author": "vimal",
"comments": "",
"dateExecuted": "2022-12-18T08:44:55.503Z",
"deploymentId": "1353095487",
"execType": "EXECUTED",
"storedChangeLog": "db/changelog/create-employee.xml",
"checksum": "8:6c2a1870c799ec202c52ee47dbd2d59d",
"orderExecuted": 1,
"contexts": [],
"labels": [],
"changeLog": "db/changelog/create-employee.xml",
"description": "createTable tableName=employee"
},
{
"id": "02",
"tag": null,
"author": "vimal",
"comments": "",
"dateExecuted": "2022-12-18T08:45:30.761Z",
"deploymentId": "1353130749",
"execType": "EXECUTED",
"storedChangeLog": "db/changelog/nullable-age.xml",
"checksum": "8:264ea35118fea6e138edbee863c37dd2",
"orderExecuted": 2,
"contexts": [],
"labels": [],
"changeLog": "db/changelog/nullable-age.xml",
"description": "dropNotNullConstraint columnName=age, tableName=employee"
}
]
}
]
Conclusion
We’ve seen that with a few configuration files we can easily track schema changes. With micronaut in dev mode, we do not need to restart the server again and again to detect the changes in the liquibase-changelog.xml. You can find the source code of this Github location.
References
https://micronaut-projects.github.io/micronaut-liquibase/latest/guide/