Database Schema Management: Implementing Versioned Migrations with Liquibase in Java

Introduction

Liquibase is an open-source, database-independent library for tracking, managing, and applying database schema changes. It enables version control for your database schema, making it possible to evolve your database structure alongside your application code in a controlled, repeatable manner.

Why Liquibase?

Key Benefits

  • Database agnostic - supports 30+ databases including PostgreSQL, MySQL, Oracle, SQL Server
  • Version control integration - schema changes are stored in source control
  • Rollback support - ability to revert changes when needed
  • Consistency - ensures all environments have the same schema
  • Collaboration - multiple developers can work on schema changes simultaneously
  • Automation - integrates with CI/CD pipelines

Setup and Dependencies

Maven Dependencies

<properties>
<liquibase.version>4.25.0</liquibase.version>
</properties>
<dependencies>
<!-- Liquibase Core -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
</dependency>
<!-- Database Drivers -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- Or for MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
</plugin>
</plugins>
</build>

Spring Boot Configuration

# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp
username: ${DB_USERNAME:app_user}
password: ${DB_PASSWORD:app_pass}
driver-class-name: org.postgresql.Driver
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml
default-schema: public
user: ${DB_USERNAME:app_user}
password: ${DB_PASSWORD:app_pass}
url: ${spring.datasource.url}
driver-class-name: ${spring.datasource.driver-class-name}
# Additional properties
drop-first: false
contexts: development
labels: !test
rollback-file: /tmp/rollback.sql
# For multiple environments
---
spring:
config:
activate:
on-profile: production
liquibase:
contexts: production
labels: production

Project Structure

src/main/resources/db/changelog/
├── db.changelog-master.yaml
├── changes/
│   ├── 001-initial-schema.yaml
│   ├── 002-add-user-roles.yaml
│   ├── 003-product-catalog.yaml
│   └── 004-order-management.yaml
├── data/
│   ├── 001-initial-data.yaml
│   └── 002-reference-data.yaml
└── views/
├── 001-user-summary-view.yaml
└── 002-sales-report-view.yaml

Core Implementation

1. Master Changelog File

# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- includeAll:
path: db/changelog/changes/
errorIfMissingOrEmpty: true
- includeAll:
path: db/changelog/data/
errorIfMissingOrEmpty: false
- includeAll:
path: db/changelog/views/
errorIfMissingOrEmpty: false
- include:
file: db/changelog/stored-procedures/001-calculate-sales.yaml
relativeToChangelogFile: true

2. Initial Schema Creation

# db/changelog/changes/001-initial-schema.yaml
databaseChangeLog:
- changeSet:
id: 001-1
author: john.doe
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: UUID
constraints:
primaryKey: true
nullable: false
defaultValueComputed: gen_random_uuid()
- column:
name: username
type: VARCHAR(50)
constraints:
unique: true
nullable: false
- column:
name: email
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: password_hash
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: first_name
type: VARCHAR(100)
- column:
name: last_name
type: VARCHAR(100)
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- column:
name: updated_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- createTable:
tableName: roles
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: VARCHAR(50)
constraints:
unique: true
nullable: false
- column:
name: description
type: VARCHAR(255)
- createTable:
tableName: user_roles
columns:
- column:
name: user_id
type: UUID
constraints:
nullable: false
foreignKeyName: fk_user_roles_user
references: users(id)
deleteCascade: true
- column:
name: role_id
type: BIGINT
constraints:
nullable: false
foreignKeyName: fk_user_roles_role
references: roles(id)
deleteCascade: true
constraints:
primaryKey:
columnNames: user_id, role_id
constraintName: pk_user_roles
- createIndex:
indexName: idx_users_email
tableName: users
columns:
- column:
name: email
descending: false
- createSequence:
sequenceName: user_id_seq
startValue: 1
incrementBy: 1

3. Adding New Tables and Modifications

# db/changelog/changes/002-add-user-roles.yaml
databaseChangeLog:
- changeSet:
id: 002-1
author: jane.smith
changes:
- addColumn:
tableName: users
columns:
- column:
name: phone_number
type: VARCHAR(20)
- column:
name: date_of_birth
type: DATE
- modifyDataType:
tableName: users
columnName: email
newDataType: VARCHAR(320)
- addNotNullConstraint:
tableName: users
columnName: first_name
columnDataType: VARCHAR(100)
- dropNotNullConstraint:
tableName: users
columnName: last_name
columnDataType: VARCHAR(100)

4. Complex Schema Changes

# db/changelog/changes/003-product-catalog.yaml
databaseChangeLog:
- changeSet:
id: 003-1
author: mike.wilson
changes:
- createTable:
tableName: categories
columns:
- column:
name: id
type: UUID
defaultValueComputed: gen_random_uuid()
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: VARCHAR(100)
constraints:
nullable: false
- column:
name: description
type: TEXT
- column:
name: parent_category_id
type: UUID
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- addForeignKeyConstraint:
baseTableName: categories
baseColumnNames: parent_category_id
constraintName: fk_categories_parent
referencedTableName: categories
referencedColumnNames: id
- createTable:
tableName: products
columns:
- column:
name: id
type: UUID
defaultValueComputed: gen_random_uuid()
constraints:
primaryKey: true
nullable: false
- column:
name: sku
type: VARCHAR(50)
constraints:
unique: true
nullable: false
- column:
name: name
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: description
type: TEXT
- column:
name: price
type: DECIMAL(10, 2)
constraints:
nullable: false
- column:
name: category_id
type: UUID
constraints:
nullable: false
- column:
name: stock_quantity
type: INTEGER
defaultValue: 0
- column:
name: is_active
type: BOOLEAN
defaultValue: true
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- column:
name: updated_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- addForeignKeyConstraint:
baseTableName: products
baseColumnNames: category_id
constraintName: fk_products_category
referencedTableName: categories
referencedColumnNames: id
- createIndex:
indexName: idx_products_sku
tableName: products
columns:
- column:
name: sku
- createIndex:
indexName: idx_products_category
tableName: products
columns:
- column:
name: category_id
- createIndex:
indexName: idx_products_active
tableName: products
columns:
- column:
name: is_active

5. Data Migrations and Seeding

# db/changelog/data/001-initial-data.yaml
databaseChangeLog:
- changeSet:
id: data-001-1
author: system
changes:
- loadData:
tableName: roles
file: db/changelog/data/roles.csv
separator: ","
relativeToChangelogFile: true
- insert:
tableName: users
columns:
- column:
name: id
value: "550e8400-e29b-41d4-a716-446655440000"
- column:
name: username
value: "admin"
- column:
name: email
value: "[email protected]"
- column:
name: password_hash
value: "$2a$10$hashedpassword"
- column:
name: first_name
value: "System"
- column:
name: last_name
value: "Administrator"
- insert:
tableName: user_roles
columns:
- column:
name: user_id
value: "550e8400-e29b-41d4-a716-446655440000"
- column:
name: role_id
value: 1
- loadUpdateData:
tableName: products
primaryKey: sku
file: db/changelog/data/products.csv
relativeToChangelogFile: true

6. Advanced Changes with Rollback

# db/changelog/changes/004-order-management.yaml
databaseChangeLog:
- changeSet:
id: 004-1
author: sarah.johnson
changes:
- createTable:
tableName: orders
columns:
- column:
name: id
type: UUID
defaultValueComputed: gen_random_uuid()
constraints:
primaryKey: true
nullable: false
- column:
name: user_id
type: UUID
constraints:
nullable: false
- column:
name: order_number
type: VARCHAR(50)
constraints:
unique: true
nullable: false
- column:
name: total_amount
type: DECIMAL(12, 2)
constraints:
nullable: false
- column:
name: status
type: VARCHAR(20)
defaultValue: "PENDING"
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- column:
name: updated_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
rollback:
- dropTable:
tableName: orders
- addForeignKeyConstraint:
baseTableName: orders
baseColumnNames: user_id
constraintName: fk_orders_user
referencedTableName: users
referencedColumnNames: id
rollback:
- dropForeignKeyConstraint:
baseTableName: orders
constraintName: fk_orders_user
- createTable:
tableName: order_items
columns:
- column:
name: id
type: UUID
defaultValueComputed: gen_random_uuid()
constraints:
primaryKey: true
nullable: false
- column:
name: order_id
type: UUID
constraints:
nullable: false
- column:
name: product_id
type: UUID
constraints:
nullable: false
- column:
name: quantity
type: INTEGER
constraints:
nullable: false
- column:
name: unit_price
type: DECIMAL(10, 2)
constraints:
nullable: false
- column:
name: total_price
type: DECIMAL(10, 2)
constraints:
nullable: false
rollback:
- dropTable:
tableName: order_items
- addForeignKeyConstraint:
baseTableName: order_items
baseColumnNames: order_id
constraintName: fk_order_items_order
referencedTableName: orders
referencedColumnNames: id
- addForeignKeyConstraint:
baseTableName: order_items
baseColumnNames: product_id
constraintName: fk_order_items_product
referencedTableName: products
referencedColumnNames: id
- createSequence:
sequenceName: order_number_seq
startValue: 1000
incrementBy: 1
rollback:
- dropSequence:
sequenceName: order_number_seq

Advanced Features

1. Custom SQL Changesets

# db/changelog/changes/005-custom-sql.yaml
databaseChangeLog:
- changeSet:
id: 005-1
author: david.brown
changes:
- sql:
sql: |
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql'
- sql:
sql: |
CREATE TRIGGER update_users_updated_at 
BEFORE UPDATE ON users 
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
- sql:
sql: |
CREATE TRIGGER update_products_updated_at 
BEFORE UPDATE ON products 
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
- sql:
sql: |
CREATE TRIGGER update_orders_updated_at 
BEFORE UPDATE ON orders 
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()
- createView:
viewName: user_order_summary
selectQuery: |
SELECT 
u.id as user_id,
u.username,
u.email,
COUNT(o.id) as total_orders,
COALESCE(SUM(o.total_amount), 0) as total_spent,
MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username, u.email
- createProcedure:
procedureName: calculate_monthly_sales
procedureText: |
CREATE OR REPLACE FUNCTION calculate_monthly_sales(year_month TEXT)
RETURNS TABLE(
product_name VARCHAR(255),
total_quantity BIGINT,
total_revenue DECIMAL(12, 2)
) AS $$
BEGIN
RETURN QUERY
SELECT 
p.name as product_name,
SUM(oi.quantity) as total_quantity,
SUM(oi.total_price) as total_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN orders o ON oi.order_id = o.id
WHERE to_char(o.created_at, 'YYYY-MM') = year_month
GROUP BY p.id, p.name
ORDER BY total_revenue DESC;
END;
$$ LANGUAGE plpgsql

2. Conditional Changesets

# db/changelog/changes/006-conditional-changes.yaml
databaseChangeLog:
- changeSet:
id: 006-1
author: lisa.green
changes:
- precondition:
onFail: MARK_RAN
dbms:
type: postgresql
- addColumn:
tableName: users
columns:
- column:
name: metadata
type: JSONB
- precondition:
onFail: MARK_RAN
sqlCheck:
expectedResult: 0
sql: SELECT COUNT(*) FROM information_schema.columns 
WHERE table_name = 'products' AND column_name = 'tags'
- addColumn:
tableName: products
columns:
- column:
name: tags
type: VARCHAR(255)[]
- precondition:
onFail: HALT
tableExists:
tableName: audit_log
- createTable:
tableName: audit_log_backup
columns:
- column:
name: id
type: BIGSERIAL
constraints:
primaryKey: true
nullable: false
- column:
name: table_name
type: VARCHAR(100)
- column:
name: record_id
type: VARCHAR(100)
- column:
name: action
type: VARCHAR(50)
- column:
name: old_values
type: JSONB
- column:
name: new_values
type: JSONB
- column:
name: changed_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- column:
name: changed_by
type: VARCHAR(100)

3. Context and Labels

# db/changelog/changes/007-context-sensitive.yaml
databaseChangeLog:
- changeSet:
id: 007-1
author: system
context: test,development
changes:
- insert:
tableName: configuration
columns:
- column:
name: key
value: "debug_mode"
- column:
name: value
value: "true"
- column:
name: environment
value: "development"
- changeSet:
id: 007-2
author: system
context: production
changes:
- insert:
tableName: configuration
columns:
- column:
name: key
value: "debug_mode"
- column:
name: value
value: "false"
- column:
name: environment
value: "production"
- changeSet:
id: 007-3
author: admin
labels: performance,indexing
changes:
- createIndex:
indexName: idx_orders_user_status
tableName: orders
columns:
- column:
name: user_id
- column:
name: status
- createIndex:
indexName: idx_order_items_order_product
tableName: order_items
columns:
- column:
name: order_id
- column:
name: product_id

Spring Boot Integration

1. Configuration Properties

package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "spring.liquibase")
public class LiquibaseProperties {
private boolean enabled = true;
private String changeLog;
private String contexts;
private String labels;
private String defaultSchema;
private String liquibaseSchema;
private String liquibaseTablespace;
private boolean dropFirst = false;
private boolean testRollbackOnUpdate = false;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getChangeLog() { return changeLog; }
public void setChangeLog(String changeLog) { this.changeLog = changeLog; }
public String getContexts() { return contexts; }
public void setContexts(String contexts) { this.contexts = contexts; }
// ... other getters and setters
}

2. Custom Liquibase Configuration

package com.example.config;
import liquibase.integration.spring.SpringLiquibase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
@Configuration
public class LiquibaseConfig {
@Autowired
private Environment environment;
@Autowired
private LiquibaseProperties liquibaseProperties;
@Bean
public SpringLiquibase liquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(dataSource);
liquibase.setChangeLog(liquibaseProperties.getChangeLog());
liquibase.setContexts(liquibaseProperties.getContexts());
liquibase.setLabels(liquibaseProperties.getLabels());
liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
liquibase.setDropFirst(liquibaseProperties.isDropFirst());
liquibase.setShouldRun(liquibaseProperties.isEnabled());
// Additional configuration
liquibase.setChangeLogParameters(Collections.singletonMap(
"environment", environment.getActiveProfiles().length > 0 ? 
environment.getActiveProfiles()[0] : "default"
));
return liquibase;
}
}

3. Programmatic Changeset Execution

package com.example.service;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.resource.ClassLoaderResourceAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
@Service
public class DatabaseMigrationService {
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigrationService.class);
private final DataSource dataSource;
private final String changeLogFile = "db/changelog/db.changelog-master.yaml";
public DatabaseMigrationService(DataSource dataSource) {
this.dataSource = dataSource;
}
public void updateDatabase() {
try (Connection connection = dataSource.getConnection()) {
Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(connection));
Liquibase liquibase = new Liquibase(changeLogFile, 
new ClassLoaderResourceAccessor(), database);
liquibase.update(new Contexts(), new LabelExpression());
logger.info("Database migration completed successfully");
} catch (Exception e) {
logger.error("Database migration failed", e);
throw new RuntimeException("Migration failed", e);
}
}
public void rollback(int changesToRollback) {
try (Connection connection = dataSource.getConnection()) {
Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(connection));
Liquibase liquibase = new Liquibase(changeLogFile, 
new ClassLoaderResourceAccessor(), database);
liquibase.rollback(changesToRollback, new Contexts(), new LabelExpression());
logger.info("Rollback completed for {} changes", changesToRollback);
} catch (Exception e) {
logger.error("Rollback failed", e);
throw new RuntimeException("Rollback failed", e);
}
}
public void generateRollbackSQL(int changesToRollback) {
try (Connection connection = dataSource.getConnection()) {
Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(connection));
Liquibase liquibase = new Liquibase(changeLogFile, 
new ClassLoaderResourceAccessor(), database);
String rollbackSql = liquibase.rollback(changesToRollback, 
new Contexts(), new LabelExpression(), 
new liquibase.changelog.ChangeLogParameters(database), 
new liquibase.output.OutputWriter() {
private final StringBuilder sql = new StringBuilder();
@Override
public void write(String message) {
sql.append(message);
}
@Override
public void close() {}
@Override
public String toString() {
return sql.toString();
}
});
logger.info("Rollback SQL generated:\n{}", rollbackSql);
} catch (Exception e) {
logger.error("Failed to generate rollback SQL", e);
throw new RuntimeException("Failed to generate rollback SQL", e);
}
}
}

Testing

1. Integration Test Configuration

package com.example.test;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.resource.ClassLoaderResourceAccessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import javax.sql.DataSource;
import java.sql.Connection;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
class LiquibaseIntegrationTest {
@Autowired
private DataSource dataSource;
@BeforeEach
void setUp() throws Exception {
// Liquibase runs automatically via Spring Boot configuration
// Additional setup if needed
}
@Test
void testDatabaseIsInitialized() throws Exception {
try (Connection connection = dataSource.getConnection()) {
// Verify tables exist
var statement = connection.createStatement();
var resultSet = statement.executeQuery(
"SELECT COUNT(*) FROM information_schema.tables " +
"WHERE table_schema = 'public'"
);
if (resultSet.next()) {
int tableCount = resultSet.getInt(1);
assertTrue(tableCount > 0, "Database should have tables created by Liquibase");
}
}
}
@Test
void testChangelogIsValid() throws Exception {
try (Connection connection = dataSource.getConnection()) {
Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(connection));
Liquibase liquibase = new Liquibase("db/changelog/db.changelog-master.yaml", 
new ClassLoaderResourceAccessor(), database);
// This will validate the changelog without applying changes
liquibase.validate();
assertTrue(true, "Changelog should be valid");
}
}
}

2. Test-Specific Changesets

# db/changelog/test/001-test-data.yaml
databaseChangeLog:
- changeSet:
id: test-001-1
author: test
context: test
changes:
- sql:
sql: "DELETE FROM users"  # Clean slate
- insert:
tableName: users
columns:
- column:
name: id
value: "11111111-1111-1111-1111-111111111111"
- column:
name: username
value: "testuser"
- column:
name: email
value: "[email protected]"
- column:
name: password_hash
value: "hashedpassword"
- column:
name: first_name
value: "Test"
- column:
name: last_name
value: "User"
- insert:
tableName: products
columns:
- column:
name: id
value: "22222222-2222-2222-2222-222222222222"
- column:
name: sku
value: "TEST-SKU-001"
- column:
name: name
value: "Test Product"
- column:
name: price
value: 99.99
- column:
name: category_id
value: "33333333-3333-3333-3333-333333333333"

Best Practices

1. Changeset Naming and Organization

# Best practice: One changeset per logical change
databaseChangeLog:
- changeSet:
id: feature-001-user-profile-extension
author: developer.name
changes:
- addColumn:
tableName: users
columns:
- column:
name: profile_picture_url
type: VARCHAR(500)
- changeSet:
id: feature-001-user-profile-index
author: developer.name
changes:
- createIndex:
indexName: idx_users_profile_picture
tableName: users
columns:
- column:
name: profile_picture_url

2. Rollback Strategy

databaseChangeLog:
- changeSet:
id: safe-table-creation
author: team
changes:
- createTable:
tableName: audit_log
columns:
# ... columns
rollback:
- dropTable:
tableName: audit_log
- changeSet:
id: data-migration-with-rollback
author: team
changes:
- sql:
sql: "INSERT INTO new_table SELECT * FROM old_table"
rollback:
- sql:
sql: "DELETE FROM new_table"
- changeSet:
id: complex-rollback
author: team
changes:
- customChange:
class: com.example.liquibase.CustomDataMigration
rollback:
- sql:
sql: "DELETE FROM migrated_data WHERE batch_id = '2024-01'"

3. Database-Specific Changes

databaseChangeLog:
- changeSet:
id: multi-database-support
author: team
changes:
- createTable:
tableName: application_log
columns:
- column:
name: id
type: BIGSERIAL
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: message
type: CLOB
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: 
postgresql: CURRENT_TIMESTAMP
mysql: NOW()
h2: CURRENT_TIMESTAMP
oracle: SYSTIMESTAMP
- addColumn:
tableName: application_log
columns:
- column:
name: severity
type: VARCHAR(20)
defaultValue: "INFO"
- modifyDataType:
tableName: application_log
columnName: message
newDataType:
postgresql: TEXT
mysql: LONGTEXT
h2: CLOB
oracle: CLOB

CI/CD Integration

1. Maven Plugin Configuration

<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
<promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
</configuration>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
</plugin>

2. Liquibase Properties

# liquibase.properties
driver=org.postgresql.Driver
url=jdbc:postgresql://localhost:5432/myapp
username=app_user
password=app_pass
changeLogFile=db/changelog/db.changelog-master.yaml
contexts=production
labels=!test
defaultSchemaName=public
liquibase.schemaName=public

Conclusion

Liquibase provides a robust, database-agnostic solution for managing schema evolution in Java applications. Key benefits include:

  • Version-controlled database changes that evolve with your application
  • Consistent environments across development, testing, and production
  • Rollback capabilities for safe deployments
  • Collaboration support for team development
  • Integration with Spring Boot and CI/CD pipelines

Best practices for successful Liquibase implementation:

  1. One logical change per changeset - keeps changes granular and manageable
  2. Meaningful changeset IDs - use descriptive names with feature references
  3. Always provide rollback instructions - enables safe deployments
  4. Use contexts and labels for environment-specific changes
  5. Test migrations thoroughly - including rollback scenarios
  6. Monitor database changelog - track what changes have been applied

By following these patterns, you can achieve reliable, repeatable database deployments that scale with your application's complexity and team size.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper