Chuyển tới nội dung chính

Quản lý schema với Liquibase

Tích hợp Liquibase vào Spring Boot kèm cơ chế tự động rollback các changeset đã áp dụng khi ứng dụng khởi động thất bại. Dùng khi service quản lý schema database bằng Liquibase.

Khi nào sử dụng

  • Service dùng Liquibase để quản lý thay đổi schema database.
  • Muốn các changeset đã áp dụng trong phiên khởi động được rollback nếu ứng dụng khởi động thất bại, tránh schema bị áp một phần.
  • Cần thay thế auto-configuration Liquibase mặc định của Spring Boot bằng cấu hình riêng của Govex.
  • Cần theo dõi trạng thái áp changeset theo môi trường triển khai (contexts, label filter).

Cài đặt

<dependency>
<groupId>vn.govex.cloud</groupId>
<artifactId>govex-cloud-liquibase</artifactId>
</dependency>

Version quản lý qua BOM — xem Cài đặt. Dependency kéo theo liquibase-core, govex-cloud-common, spring-data-jdbcspring-boot-autoconfigure.

Tích hợp trong starter

Auto-configuration

LiquibaseAutoConfiguration là auto-configuration của module với các điều kiện:

  • SpringLiquibaseDatabaseChange trên classpath;
  • spring.liquibase.enabled khác false (mặc định bật);
  • có nguồn dữ liệu để migrate: bean DataSource, bean JdbcConnectionDetails, hoặc spring.liquibase.url.

Auto-configuration chạy sau DataSourceAutoConfigurationHibernateJpaAutoConfiguration, đồng thời đặt trước auto-configuration Liquibase mặc định của Spring Boot để thay thế nó.

Chọn datasource migration

Khi tạo SpringLiquibase, module chọn datasource theo thứ tự ưu tiên:

  1. bean DataSource đánh dấu @LiquibaseDataSource (nếu có);
  2. datasource tạo mới từ spring.liquibase.url khi có URL;
  3. datasource tạo mới từ DataSource hiện có khi có spring.liquibase.user;
  4. datasource chính của ứng dụng.

Với hai nhánh tạo mới, module dùng SimpleDriverDataSource; nếu datasource này không phải datasource sẵn có thì dùng DataSourceClosingSpringLiquibase để đóng kết nối sau khi migrate.

Bean SpringLiquibase

Bean SpringLiquibase chỉ được tạo khi ứng dụng chưa có bean cùng loại, nên có thể thay thế bằng cấu hình riêng nếu cần. Module đồng thời:

  • gắn Customizer<Liquibase> để đăng ký toàn bộ bean ChangeExecListener vào Liquibase qua DefaultChangeExecListener;
  • đăng ký sẵn bean LiquibaseChangeExecListener để ghi log và lưu vết changeset đã chạy;
  • lưu datasource đã dùng cho migration vào LiquibaseRollbackHolder phục vụ rollback khi khởi động lỗi.

Property spring.liquibase.* được áp dụng

Module đọc property chuẩn của Spring Boot và truyền vào SpringLiquibase:

PropertyMô tả
spring.liquibase.change-logĐường dẫn changelog master
spring.liquibase.enabledBật/tắt migration; đồng thời là điều kiện của auto-configuration
spring.liquibase.contextsDanh sách context được chạy
spring.liquibase.label-filterBộ lọc label
spring.liquibase.default-schemaSchema mặc định
spring.liquibase.liquibase-schemaSchema chứa bảng quản lý của Liquibase
spring.liquibase.database-change-log-tableTên bảng theo dõi changeset
spring.liquibase.database-change-log-lock-tableTên bảng khóa
spring.liquibase.drop-firstXóa toàn bộ đối tượng database trước khi chạy
spring.liquibase.clear-checksumsXóa checksum đã lưu
spring.liquibase.parametersTham số changelog
spring.liquibase.rollback-fileFile nhận script rollback
spring.liquibase.test-rollback-on-updateThử rollback khi update
spring.liquibase.tagTag gắn cho database sau khi update

Cấu hình

Dependency dùng các property chuẩn của Spring Boot (spring.liquibase.*) và khai báo thêm:

PropertyMô tảMặc định
govex.liquibase.auto-rollbackBật/tắt tự động rollback changeset khi ứng dụng khởi động thất bạitrue
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.xml
contexts: ${spring.profiles.active:development}
default-schema: public
govex:
liquibase:
auto-rollback: true

Quan trọng: hiện tại govex.liquibase.auto-rollback chưa được nối vào auto-configuration. Class vn.govex.cloud.liquibase.autoconfigure.LiquibaseProperties có nhận property này, nhưng:

  • @EnableConfigurationProperties trong LiquibaseAutoConfiguration đang trỏ vào org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties (khai báo import che class cùng package), nên class property của Govex không được đăng ký thành bean và giá trị cấu hình không được bind;
  • nhánh kiểm tra cờ trong LiquibaseRollbackListener đang bị comment.

Hệ quả: cơ chế rollback khi ứng dụng khởi động thất bại vẫn diễn ra bất kể giá trị cấu hình; không nên dựa vào cờ này để tắt rollback. Metadata cấu hình của module vẫn khai báo property nên IDE vẫn gợi ý nó.

Cấu trúc changelog

Đặt changelog master trong src/main/resources/db/changelog/ và include các file change theo phiên bản hoặc theo thời gian; cấu trúc thư mục do ứng dụng tự quyết định:

src/main/resources
└── db
└── changelog
├── db.changelog-master.xml
├── changes
│ ├── 001-create-tables.xml
│ └── 002-insert-initial-data.xml
└── includes
└── common-changeset.xml
<!-- db/changelog/db.changelog-master.xml -->
<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="changes/001-create-tables.xml" relativeToChangelogFile="true"/>
<!-- hoặc includeAll path="changes/" relativeToChangelogFile="true" -->
</databaseChangeLog>

Có thể dùng include với relativeToChangelogFile="true" để đường dẫn tính theo file master, hoặc includeAll để tự động include toàn bộ file trong thư mục theo thứ tự tên. Changelog nhận các định dạng XML, YAML, JSON và SQL (--liquibase formatted sql).

Include changelog của module

Một số module trong hệ Govex đóng gói sẵn changelog, ứng dụng include vào changelog master của mình để chạy migration của module:

ModuleĐường dẫn changelog
govex-cloud-workflowdb/changelog/workflow/changelog-master.yaml
govex-cloud-templatedb/changelog/template/changelog-master.yaml
govex-cloud-configdb/changelog/config/changelog-master.yaml
govex-cloud-transactiondb/changelog/transaction/changelog-master.yaml
<!-- trong db.changelog-master.xml của ứng dụng -->
<include file="classpath:db/changelog/workflow/changelog-master.yaml"/>

Nếu ứng dụng chỉ dùng một module, có thể trỏ thẳng spring.liquibase.change-log tới changelog master của module đó. Các changeset trong module thường dùng preConditions kiểm tra bảng đã tồn tại để bỏ qua an toàn:

databaseChangeLog:
- changeSet:
id: 000-workflow-process-definition
author: govex-cloud
preConditions:
- onFail: MARK_RAN
- not:
- tableExists:
tableName: wf_process_definition
changes:
- createTable:
tableName: wf_process_definition
columns:
- column: { name: ID, type: varchar(36), constraints: { primaryKey: true, nullable: false } }

Rollback khi khởi động thất bại

Ghi nhận changeset đã chạy

LiquibaseChangeExecListener kế thừa AbstractChangeExecListener của Liquibase và được nối vào Liquibase qua Customizer. Khi một changeset được thực thi xong, listener ghi nhận vào LiquibaseRollbackHolder:

  • EXECUTED — changeset vừa được áp dụng: ghi log và đẩy vào holder;
  • MARK_RAN — changeset được đánh dấu đã chạy (thường do precondition MARK_RAN): đẩy vào holder;
  • SKIPPED — bỏ qua, chỉ ghi log.

LiquibaseRollbackHolder giữ một Stack<RanChangeSet> static và datasource đã dùng để migration; RanChangeSet lưu cặp ChangeSet + DatabaseChangeLog để có đủ ngữ cảnh rollback.

Listener rollback

LiquibaseRollbackListener được đăng ký qua META-INF/spring.factories với key org.springframework.context.ApplicationListener, nên hoạt động theo sự hiện diện trên classpath. Listener bắt ApplicationFailedEvent (ứng dụng khởi động thất bại) và thực hiện:

  1. Ghi log nguyên nhân khởi động thất bại.
  2. Bỏ qua nếu chưa có changeset nào được áp trong phiên hiện tại (hasChangelogApplied()).
  3. Tạo một SimpleDriverDataSource mới từ datasource đã lưu trong holder, tránh dùng lại connection pool đang bị đóng khi context lỗi.
  4. Lấy lần lượt từng RanChangeSet từ stack (LIFO — rollback theo thứ tự ngược lại thứ tự áp dụng) và gọi RollbackVisitor.visit(...) với một connection mới cho mỗi changeset.
  5. Ghi log thành công cho từng changeset; cuối cùng đóng datasource tạm bằng reflection nếu lớp có method close().

Các lỗi khi rollback (RollbackFailedException, SQLException hoặc ngoại lệ khác) đều được ghi log và không ném lại — vì ứng dụng đã ở trạng thái khởi động thất bại.

Luồng hoạt động

Sơ đồ dưới đây mô tả luồng tích hợp: Liquibase chạy changelog khi khởi động, ghi vết changeset đã áp, và rollback theo thứ tự ngược lại nếu khởi động thất bại.

Ví dụ changelog

ChangeSet tạo bảng có rollback tự sinh:

<changeSet id="create-role-table" author="thiennv93">
<createTable tableName="roles">
<column name="id" type="int">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(50)"/>
</createTable>
<!-- không cần thẻ rollback, Liquibase tự sinh DROP TABLE -->
</changeSet>

ChangeSet chèn dữ liệu cần rollback thủ công:

<changeSet id="insert-default-roles" author="thiennv93">
<insert tableName="roles">
<column name="id" valueNumeric="1"/>
<column name="name" value="ADMIN"/>
</insert>
<rollback>
DELETE FROM roles WHERE id = 1;
</rollback>
</changeSet>

ChangeSet kiểm soát thực thi bằng precondition:

<changeSet id="create-user-data" author="thiennv93">
<preConditions onFail="MARK_RAN" onError="HALT">
<tableExists tableName="users"/>
<not>
<columnExists tableName="users" columnName="name"/>
</not>
</preConditions>
<addColumn tableName="users">
<column name="name" type="varchar(255)"/>
</addColumn>
</changeSet>

Lệnh Maven thường dùng

# Áp dụng các changeset chưa chạy
mvn liquibase:update

# Kiểm tra trạng thái changeset
mvn liquibase:status

# Rollback 1 changeset gần nhất
mvn liquibase:rollback -Dliquibase.rollbackCount=1

# Đánh dấu toàn bộ changeset trong changelog là đã áp dụng (khi tích hợp vào database có sẵn)
mvn liquibase:changelogSync

Kiểm thử rollback

Demo order-service trong repo nguồn minh họa cách mô phỏng khởi động thất bại để kiểm tra cơ chế rollback: một bean @Configuration đọc liquibase.test.failure.enabled và ném RuntimeException trong @PostConstruct.

liquibase:
test:
failure:
enabled: true
message: "Mô phỏng lỗi ứng dụng để kiểm tra tính năng tự động rollback Liquibase"

Sau khi khởi động lỗi, các changeset vừa được áp sẽ bị rollback theo cơ chế mô tả ở trên.

Lưu ý

  • Ứng dụng tự khai báo changelog của mình; module chỉ cung cấp tích hợp và cơ chế rollback.
  • Cấu hình tự động chỉ chạy khi có nguồn dữ liệu để migrate: DataSource, JdbcConnectionDetails hoặc spring.liquibase.url.
  • Cơ chế rollback dựa trên thông tin rollback của từng changeset; nên viết changeset có thể rollback để tránh lỗi khi khởi động thất bại.
  • LiquibaseRollbackHolder dùng biến static và không được dọn sau khi khởi động thành công. Trong cùng một JVM (ví dụ devtools restart), danh sách changeset đã áp của phiên trước vẫn còn, nên lần khởi động lỗi kế tiếp có thể rollback cả changeset không thuộc phiên hiện tại — cần đặc biệt chú ý khi chạy ở môi trường có restart nóng.
  • Nếu một changeset rollback lỗi, vòng lặp dừng lại: các changeset còn lại trong stack chưa được xử lý và chỉ được ghi log.
  • Khi ứng dụng tự khai báo bean SpringLiquibase, các bean listener/ghi vết của module không được tạo theo, nên cơ chế rollback tự động không hoạt động.
  • Đặt spring.liquibase.enabled=false sẽ tắt luôn migration của Govex vì đây là điều kiện của auto-configuration.