Flyway core & setup — schema versioning như code
Flyway version hoá schema database: mỗi thay đổi là 1 SQL file đánh số, apply đúng 1 lần theo thứ tự, checksum bất biến sau khi apply. Bài này bóc cơ chế flyway_schema_history, naming convention V__migration, setup Boot, và baseline existing DB — cùng lý do tại sao forward-only là lựa chọn thiết kế đúng.
TL;DR: Flyway giải quyết vấn đề ddl-auto=update không version hoá được: mỗi schema change là 1 file SQL tên V<version>__<mo_ta>.sql, apply đúng 1 lần theo thứ tự, checksum được lưu vào flyway_schema_history để audit. Sau khi apply, không được sửa file — checksum bất biến là cơ chế đảm bảo mọi môi trường có cùng lịch sử migration. Boot autoconfigure Flyway qua flyway-core dependency. DB legacy chưa có Flyway dùng baseline-on-migrate: true để onboard mà không phá schema cũ.
ddl-auto=update là anti-pattern trên production vì nó không để lại audit trail và không đảm bảo reproducibility — bài JPA & Hibernate — ddl-auto strategy đã phân tích 6 rủi ro. Bài này bóc đúng 1 thứ: Flyway core và cách nó biến schema DB thành source-controllable artifact, không lan sang CI/CD hay multi-environment pattern (bài tiếp theo).
1. Vì sao schema migration — versioning DB như code
Hình dung team 3 dev cùng nhau làm TaskFlow. Dev A thêm column priority, Dev B thêm table tasks. Nếu mỗi người chỉ sửa entity Java và để ddl-auto=update tự điều, điều gì xảy ra khi deploy production?
Dev A → entity Project có field priority → Hibernate ADD COLUMN priority
Dev B → entity Task mới → Hibernate CREATE TABLE tasks
Dev C → rename field deadline → due_date → Hibernate KHÔNG rename
→ DROP deadline, ADD due_date
→ DATA LOSS — dữ liệu cột deadline mất
ddl-auto=update có 4 giới hạn cốt lõi khiến nó không phù hợp production:
| Giới hạn | Hệ quả |
|---|---|
| Không version | Không biết DB đang ở "phiên bản" nào — không audit trail |
| Không drop / rename | Schema bẩn — cột cũ tồn tại vĩnh viễn |
| Race condition | 2 pod startup đồng thời → conflict ALTER TABLE |
| Không reproducible | Schema phụ thuộc Hibernate version + entity scan order |
Migration tool giải quyết bằng 3 nguyên tắc:
- Versioned — mỗi change có số version, apply đúng 1 lần, theo thứ tự.
- Reproducible — SQL script là source of truth, chạy trên bất kỳ env nào cho cùng kết quả.
- Audited — lịch sử mọi thay đổi lưu trong DB, có timestamp và checksum.
2. Flyway core concepts
2.1 Naming convention — quy tắc đặt tên file bắt buộc
Flyway nhận diện migration script hoàn toàn qua tên file. Đây là convention bắt buộc:
V<version>__<mo_ta>.sql
^ ^^
| hai gach duoi (double underscore)
version (1, 1.1, 20260415_1430, ...)
Ví dụ thực tế trong TaskFlow:
src/main/resources/db/migration/
├── V1__init_schema.sql
├── V2__add_project_priority.sql
├── V3__create_tasks_table.sql
└── V4__add_task_assignee.sql
V1__init_schema.sql đúng (double underscore). V1_init_schema.sql sai — Flyway không nhận diện, migration không được apply. Lỗi âm thầm, không throw exception.
Ngoài versioned migration (V), Flyway có 2 loại khác:
| Prefix | Loại | Chạy khi nào |
|---|---|---|
V<version>__<desc>.sql | Versioned | Một lần, theo thứ tự version |
R__<desc>.sql | Repeatable | Khi checksum file thay đổi |
U<version>__<desc>.sql | Undo | Rollback thủ công (Flyway Teams edition — có phí) |
R__ dùng cho view, stored procedure — những thứ được thay đổi nhiều lần. Bài này tập trung V__ vì đó là core workflow.
2.2 flyway_schema_history — cơ chế theo dõi
Khi Flyway chạy lần đầu, nó tự tạo table flyway_schema_history trong cùng DB:
-- Flyway tự tao, khong can viet tay
SELECT installed_rank, version, description, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
-- installed_rank | version | description | checksum | success
-- 1 | 1 | init schema | -123456789 | true
-- 2 | 2 | add project priority | -987654321 | true
-- 3 | 3 | create tasks table | 456789012 | true
Mỗi row là một migration đã apply. Các trường quan trọng:
version— version string từ tên file (V3__...→"3").checksum— CRC32 của nội dung file lúc apply. Đây là trường bất biến sau khi apply.success—truenếu apply thành công,falsenếu fail giữa chừng.
Khi app khởi động, Flyway so sánh danh sách file trong db/migration/ với flyway_schema_history:
flowchart LR
subgraph Files["db/migration/ (filesystem)"]
direction TB
F1["V1__init.sql"]
F2["V2__priority.sql"]
F3["V3__tasks.sql"]
end
subgraph History["flyway_schema_history (DB)"]
direction TB
H1["V1 - checksum aaa - success"]
H2["V2 - checksum bbb - success"]
end
subgraph Action["Flyway decision"]
direction TB
A1["V1: da co trong history -> skip"]
A2["V2: da co trong history -> skip"]
A3["V3: CHUA co -> APPLY"]
end
Files --> Action
History --> ActionKết quả: chỉ V3 được apply. V1 và V2 bỏ qua vì đã có trong history. Đây là tính chất idempotent — restart app bao nhiêu lần cũng an toàn.
2.3 Checksum bất biến — tại sao không được sửa file đã apply
Đây là rule quan trọng nhất của Flyway, và có lý do thiết kế rõ ràng:
Tình huống: V2 đã apply trên production (checksum = -987654321). Dev sửa file V2__add_project_priority.sql để thêm 1 dòng nữa. Restart app:
flowchart TB
subgraph Check["Flyway startup check"]
direction LR
C1["Doc V2__add_project_priority.sql (da sua)"]
C2["Tinh checksum moi: -111111111"]
C3["So sanh vs history: -987654321"]
C4["MISMATCH"]
C5["FlywayException: STARTUP FAIL"]
C1 --> C2 --> C3 --> C4 --> C5
endApp không khởi động. Đây là intentional fail-fast — không phải bug của Flyway.
Tại sao checksum phải bất biến? Vì nếu cho phép sửa file đã apply, bạn mất đi tính reproducibility: env A apply V2 gốc, env B apply V2 đã sửa → hai DB có schema khác nhau trong khi Flyway history nói cả hai đều "ở version 2". Audit trail vô nghĩa. Checksum immutability là hợp đồng bảo toàn "cùng history = cùng schema".
Cách đúng: nếu cần thêm thay đổi, viết migration mới V3__... thay vì sửa V2:
✅ DUNG:
V2__add_project_priority.sql (giu nguyen)
V3__fix_priority_default.sql (them migration moi)
❌ SAI:
V2__add_project_priority.sql (da sua noi dung) → checksum mismatch → FAIL
Ngoại lệ hợp lệ duy nhất: migration trên feature branch chưa merge vào main — chưa apply trên môi trường nào khác ngoài local của bạn → sửa thoải mái. Ngay khi merge vào main và apply trên staging/production, file trở thành bất biến.
2.4 Concurrent-safe — DB lock
Khi nhiều pod khởi động đồng thời (rolling deploy Kubernetes), Flyway đảm bảo chỉ 1 pod apply migration:
Pod A startup → Flyway acquire DB lock → apply V3 → release lock
Pod B startup → Flyway try acquire lock → WAIT → lock released
→ check history → V3 da apply
→ no-op, skip
Cơ chế là advisory lock của PostgreSQL (pg_advisory_lock). Không cần cấu hình — Flyway tự handle.
3. Setup Spring Boot
3.1 Dependency
<!-- pom.xml -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<!-- version managed by Spring Boot BOM -->
</dependency>
<!-- PostgreSQL support (Boot 3.4+) -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
flyway-core là Apache 2.0 (miễn phí). flyway-database-postgresql là module mở rộng cho PostgreSQL dialect — cũng miễn phí.
Khi có flyway-core trên classpath và spring.datasource được configure, Boot tự tạo Flyway bean và gọi migrate() trước khi app nhận request đầu tiên. Không cần viết @Bean Flyway thủ công.
3.2 Configuration
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration # default, co the bo qua
validate-on-migrate: true # checksum verify khi startup
jpa:
hibernate:
ddl-auto: validate # Flyway quan ly schema, Hibernate chi validate
ddl-auto: validate là pattern chuẩn kết hợp với Flyway:
- Flyway apply migration → schema được tạo/cập nhật.
- Hibernate validate entity mapping với schema → fail fast nếu mismatch (ví dụ entity có field mà DB không có column).
Không dùng ddl-auto: update kết hợp Flyway — hai bên cùng sửa schema gây race condition.
3.3 Migration đầu tiên
-- src/main/resources/db/migration/V1__init_schema.sql
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'PLANNING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_project_name UNIQUE (name),
CONSTRAINT chk_project_status
CHECK (status IN ('PLANNING', 'ACTIVE', 'DONE', 'ARCHIVED'))
);
CREATE INDEX idx_projects_status ON projects(status);
CREATE INDEX idx_projects_created ON projects(created_at DESC);
App khởi động lần đầu:
- Flyway tạo
flyway_schema_history(nếu chưa có). - Phát hiện
V1__init_schema.sqlchưa có trong history. - Chạy script, tạo table
projectsvà 2 index. - Ghi row vào history: version
1, checksum tính từ nội dung file,success = true.
Khởi động lần hai: V1 đã có trong history → skip.
3.4 Thêm column — migration V2
-- src/main/resources/db/migration/V2__add_project_priority.sql
ALTER TABLE projects
ADD COLUMN priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM';
CREATE INDEX idx_projects_priority ON projects(priority);
Best practice khi thêm column NOT NULL:
- Luôn kèm
DEFAULTvalue để backward-compatible với existing rows. - Add index trong cùng migration nếu liên quan — tránh state inconsistent giữa column và index khi migration fail.
4. Baseline existing DB
Tình huống thực tế: TaskFlow đã chạy production 6 tháng với schema 15 table, chưa có Flyway. Cần onboard Flyway mà không xoá data.
Vấn đề: Flyway thấy flyway_schema_history không có → tưởng DB mới hoàn toàn → cố chạy V1 → V1 tạo table đã tồn tại → ERROR.
Giải pháp: baseline-on-migrate: true.
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1
baseline-description: "Existing schema as of 2026-06-09"
Cơ chế hoạt động:
flowchart TB
subgraph FirstDeploy["Lan deploy dau tien (existing DB)"]
direction TB
D1["Flyway connect DB"]
D2["flyway_schema_history khong ton tai"]
D3["baseline-on-migrate=true: tao table + insert BASELINE row"]
D4["version=1, type=BASELINE, success=true"]
D5["So sanh files: V1 da co baseline -> skip<br/>V2 pending -> APPLY"]
D1 --> D2 --> D3 --> D4 --> D5
endSau deploy đầu tiên:
SELECT version, description, type FROM flyway_schema_history;
-- 1 Existing schema as of 2026-06-09 BASELINE
-- 2 add project priority SQL
Production DB có schema cũ nhảy thẳng vào Flyway-managed. V1 baseline chỉ là marker — Flyway không chạy nội dung file V1 trên DB này (vì baseline version = 1, mọi script version từ 1 trở xuống được skip).
Workflow onboard đầy đủ:
1. pg_dump --schema-only prod > current-schema.sql
(snapshot schema hien tai de luu tru)
2. Tao V1__baseline.sql -- copy noi dung current-schema.sql
(de new env (dev, test) co the apply tu dau)
3. Them V2__... cho change tiep theo
4. Configure baseline-on-migrate: true
5. Deploy: production DB nhan baseline marker, apply V2+
New env (dev fresh): apply V1 (full schema) + V2+
-- ca hai deu dat cung schema cuoi
File V1__baseline.sql sẽ được apply trên fresh env (test DB, dev mới). Mọi CREATE TABLE phải có IF NOT EXISTS, mọi CREATE INDEX tương tự — phòng trường hợp script chạy 2 lần do restart giữa chừng.
Cơ chế bên dưới — luồng startup Flyway
Khi Spring Boot khởi động và gọi Flyway.migrate(), luồng thực tế như sau:
flowchart TB
A["Flyway.migrate()"] --> B["Acquire DB advisory lock"]
B --> C["Ensure flyway_schema_history exists"]
C --> D["Scan classpath:db/migration/ cho V*.sql"]
D --> E["Filter: chi lay scripts CHUA co trong history"]
E --> F{{"Pending scripts?<br/>con scripts chua apply?"}}
F -->|"Khong"| G["Release lock, no-op"]
F -->|"Co"| H["Apply theo thu tu version"]
H --> I["Tinh checksum, ghi history row, success=true"]
I --> J["Con pending khac?"]
J -->|"Co"| H
J -->|"Khong"| GBa điểm cốt lõi rút ra từ luồng này:
- Lock trước, apply sau — đảm bảo concurrent-safe dù 100 pod khởi động đồng thời.
- Filter theo history — chỉ apply pending, idempotent với restart.
- Checksum ghi sau khi apply thành công — nếu script fail,
success = falsetrong history, app không start, DBA có thể diagnose rồi chạyflyway repair.
Pitfall của riêng concept này
❌ Nhầm 1 — Sửa migration đã apply:
-- V2__add_project_priority.sql (da apply production)
ALTER TABLE projects ADD COLUMN priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM';
ALTER TABLE projects ADD COLUMN owner_id BIGINT; -- THEM VAO SAU KHI DA APPLY
Kết quả: FlywayException: Migration checksum mismatch for migration version 2 → app không khởi động.
✅ Tạo V3__add_project_owner.sql riêng. V2 giữ nguyên bất biến mãi mãi.
❌ Nhầm 2 — Một gạch dưới thay vì hai:
V1_init_schema.sql ← SAI: Flyway khong nhan dien, bo qua
V1__init_schema.sql ← DUNG: double underscore
Flyway không throw error — nó chỉ không thấy file. Schema không được tạo, Hibernate validate fail với SchemaExportException. Debug rất confusing vì lỗi báo ở Hibernate chứ không phải Flyway.
❌ Nhầm 3 — Dùng ddl-auto=update cùng Flyway:
spring:
flyway:
enabled: true
jpa:
hibernate:
ddl-auto: update # SAI khi da dung Flyway
Hai bên cùng sửa schema → race condition: Flyway apply V2 thêm column, Hibernate update thấy column chưa đúng type và cũng ALTER → conflict. Luôn dùng ddl-auto: validate khi có Flyway.
❌ Nhầm 4 — Quên baseline-on-migrate khi onboard DB cũ:
spring:
flyway:
enabled: true
# baseline-on-migrate: true ← QUEN
Kết quả: Flyway thấy DB không có flyway_schema_history → chạy V1 → CREATE TABLE projects trên table đã tồn tại → ERROR: relation "projects" already exists → app không start.
📚 Deep Dive
Flyway:
- Flyway Migration Naming Convention — naming rules, version format, prefix semantics.
- Flyway Schema History — structure của
flyway_schema_history, checksum algorithm.
Spring Boot:
- Spring Boot — Flyway Integration — autoconfigure,
FlywayProperties, baseline config. - Spring Boot Auto-configuration Report —
FlywayAutoConfigurationsource.
ddl-auto deep dive:
- Bài JPA & Hibernate — ddl-auto strategy section 8 — 5 mode, khi nào dùng mode nào, tại sao
validatelà production default.
Liên hệ các bài khác
- JPA & Hibernate — ddl-auto: Bài này thiết lập tại sao
ddl-auto=updatekhông đủ production-grade vàddl-auto=none/validatelà lựa chọn đúng — Flyway là thứ lấp chỗ trống schema management đó. - Migration patterns & CI/CD: Bài tiếp theo mở rộng từ core sang production workflow: forward-only rollback, multi-environment config (
dev/,prod/), CI pipeline test migration, và zero-downtime expand-contract pattern.
Tóm tắt
ddl-auto=updatekhông phù hợp production vì thiếu versioning, không drop/rename, race condition, không reproducible.- Flyway giải quyết bằng migration file
V<version>__<mo_ta>.sql— apply đúng 1 lần, theo thứ tự, ghi vàoflyway_schema_history. - Checksum bất biến sau khi apply — sửa file đã apply gây
FlywayExceptionstartup fail. Đây là intentional, bảo toàn tính "cùng history = cùng schema" giữa mọi môi trường. - Boot autoconfigure qua
flyway-core+flyway-database-postgresqldependency. Pattern chuẩn:ddl-auto: validate+ Flyway. - DB legacy onboard bằng
baseline-on-migrate: true— production DB nhận BASELINE marker, skip V1, apply V2+. Fresh env apply V1 full schema + V2+. - Concurrent-safe qua DB advisory lock — 100 pod startup đồng thời, chỉ 1 pod apply migration.
Tự kiểm tra
Q1Vì sao Flyway thiết kế checksum bất biến — tức là không cho sửa file migration sau khi đã apply? Hệ quả nếu cho phép sửa là gì?▸
Checksum bất biến là hợp đồng bảo toàn tính "cùng history = cùng schema": nếu hai env đều có flyway_schema_history ghi version 2 với cùng checksum, hai DB chắc chắn có cùng schema tại version 2.
Nếu cho phép sửa file đã apply: env A apply V2 gốc (checksum X), dev sửa V2, env B apply V2 mới (checksum Y) — cả hai đều nói "ở version 2" nhưng schema thực tế khác nhau. Audit trail vô nghĩa, bug production rất khó reproduce vì schema dev và staging lệch nhau âm thầm.
Flyway chọn fail-fast — throw FlywayException khi phát hiện checksum mismatch — thay vì để inconsistency âm thầm lan ra. Đây là quyết định thiết kế ưu tiên correctness over convenience.
Q2Phân biệt flyway_schema_history với schema thật của app. Flyway dùng table đó để làm gì khi app restart?▸
flyway_schema_history với schema thật của app. Flyway dùng table đó để làm gì khi app restart?flyway_schema_history là metadata table của Flyway — lưu lịch sử các migration đã apply (version, checksum, timestamp, success). Nó không chứa business data — chỉ là "sổ cái" theo dõi lịch sử schema.
Khi app restart, Flyway đọc table này để biết DB đang ở trạng thái nào. Sau đó scan db/migration/ và chỉ apply các script chưa có trong history. Nếu V1, V2, V3 đã có → skip cả 3, chỉ apply V4 nếu tồn tại.
Tính chất này khiến Flyway idempotent: restart app bao nhiêu lần, deploy bao nhiêu pod, schema chỉ thay đổi đúng 1 lần cho mỗi migration version.
Q3Team đang onboard Flyway cho app đã chạy production 1 năm (20 table, không có flyway_schema_history). Nếu chỉ thêm flyway-core dependency và tạo V1__init_schema.sql mà không config baseline-on-migrate, điều gì xảy ra khi deploy?▸
flyway_schema_history). Nếu chỉ thêm flyway-core dependency và tạo V1__init_schema.sql mà không config baseline-on-migrate, điều gì xảy ra khi deploy?App sẽ fail khởi động. Flyway thấy flyway_schema_history không tồn tại → tưởng DB hoàn toàn mới → cố chạy V1__init_schema.sql → script có CREATE TABLE projects → PostgreSQL throw ERROR: relation "projects" already exists → Flyway đánh dấu migration success=false → app không start.
Fix đúng: thêm baseline-on-migrate: true + baseline-version: 1. Khi đó Flyway detect không có history → insert BASELINE row cho version 1 (không chạy V1 script) → so sánh files: V1 đã có baseline, skip → chỉ apply V2+ nếu có.
File V1 vẫn cần giữ để fresh env (dev local, test) có thể apply full schema từ đầu. Hai env đạt cùng schema cuối: production qua baseline + V2+, dev fresh qua V1 + V2+.
Q4Developer tạo file V3_add_tasks_table.sql (một gạch dưới). App khởi động, không có lỗi, nhưng table tasks không được tạo. Tại sao? Sửa thế nào?▸
V3_add_tasks_table.sql (một gạch dưới). App khởi động, không có lỗi, nhưng table tasks không được tạo. Tại sao? Sửa thế nào?Flyway nhận diện migration script hoàn toàn qua tên file theo pattern V<version>__<desc>.sql với double underscore. File V3_add_tasks_table.sql (single underscore) không khớp pattern → Flyway bỏ qua hoàn toàn, không throw warning hay error.
App khởi động thành công vì Flyway không thấy migration pending — nó đơn giản không nhận ra file đó là migration. Hibernate validate sau đó báo lỗi SchemaExportException hoặc app chạy nhưng query tới tasks throw Table "tasks" does not exist.
Fix: đổi tên file thành V3__add_tasks_table.sql (double underscore). Nếu app đang chạy local, Flyway sẽ detect V3 là pending và apply ngay lần restart tiếp.
Q5Tại sao pattern ddl-auto: validate + Flyway tốt hơn chỉ dùng Flyway một mình (với ddl-auto: none)?▸
ddl-auto: validate + Flyway tốt hơn chỉ dùng Flyway một mình (với ddl-auto: none)?ddl-auto: none — Hibernate không làm gì với schema. App vẫn chạy ngay cả khi entity mapping và DB schema lệch nhau (ví dụ entity thêm field nhưng quên viết migration). Lỗi chỉ xuất hiện khi query thực sự chạy đến column đó — có thể là lúc production, không phải startup.
ddl-auto: validate — Hibernate so sánh entity mapping với schema hiện tại ngay khi khởi động. Nếu entity có field priority nhưng DB column chưa tồn tại (migration bị quên), app không start ngay lập tức với message rõ ràng.
Kết hợp: Flyway đảm bảo schema đúng version, Hibernate validate đảm bảo entity Java match schema đó. Hai lớp fail-fast bổ sung nhau — lỗi lộ ra lúc deploy chứ không phải lúc user đang dùng.
Bài tiếp theo: Migration patterns & CI/CD
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên