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-jdbc và spring-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:
- có
SpringLiquibasevàDatabaseChangetrên classpath; spring.liquibase.enabledkhácfalse(mặc định bật);- có nguồn dữ liệu để migrate: bean
DataSource, beanJdbcConnectionDetails, hoặcspring.liquibase.url.
Auto-configuration chạy sau DataSourceAutoConfiguration và HibernateJpaAutoConfiguration, đồ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:
- bean
DataSourceđánh dấu@LiquibaseDataSource(nếu có); - datasource tạo mới từ
spring.liquibase.urlkhi có URL; - datasource tạo mới từ
DataSourcehiện có khi cóspring.liquibase.user; - 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ộ beanChangeExecListenervào Liquibase quaDefaultChangeExecListener; - đă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
LiquibaseRollbackHolderphụ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:
| Property | Mô tả |
|---|---|
spring.liquibase.change-log | Đường dẫn changelog master |
spring.liquibase.enabled | Bật/tắt migration; đồng thời là điều kiện của auto-configuration |
spring.liquibase.contexts | Danh sách context được chạy |
spring.liquibase.label-filter | Bộ lọc label |
spring.liquibase.default-schema | Schema mặc định |
spring.liquibase.liquibase-schema | Schema chứa bảng quản lý của Liquibase |
spring.liquibase.database-change-log-table | Tên bảng theo dõi changeset |
spring.liquibase.database-change-log-lock-table | Tên bảng khóa |
spring.liquibase.drop-first | Xóa toàn bộ đối tượng database trước khi chạy |
spring.liquibase.clear-checksums | Xóa checksum đã lưu |
spring.liquibase.parameters | Tham số changelog |
spring.liquibase.rollback-file | File nhận script rollback |
spring.liquibase.test-rollback-on-update | Thử rollback khi update |
spring.liquibase.tag | Tag 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:
| Property | Mô tả | Mặc định |
|---|---|---|
govex.liquibase.auto-rollback | Bật/tắt tự động rollback changeset khi ứng dụng khởi động thất bại | true |
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:
@EnableConfigurationPropertiestrongLiquibaseAutoConfigurationđang trỏ vàoorg.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-workflow | db/changelog/workflow/changelog-master.yaml |
govex-cloud-template | db/changelog/template/changelog-master.yaml |
govex-cloud-config | db/changelog/config/changelog-master.yaml |
govex-cloud-transaction | db/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 preconditionMARK_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:
- Ghi log nguyên nhân khởi động thất bại.
- Bỏ qua nếu chưa có changeset nào được áp trong phiên hiện tại (
hasChangelogApplied()). - Tạo một
SimpleDriverDataSourcemới từ datasource đã lưu trong holder, tránh dùng lại connection pool đang bị đóng khi context lỗi. - Lấy lần lượt từng
RanChangeSettừ stack (LIFO — rollback theo thứ tự ngược lại thứ tự áp dụng) và gọiRollbackVisitor.visit(...)với một connection mới cho mỗi changeset. - 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,JdbcConnectionDetailshoặcspring.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.
LiquibaseRollbackHolderdù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=falsesẽ tắt luôn migration của Govex vì đây là điều kiện của auto-configuration.