Flyway Migration Spring Boot — version schema như code
Setup Flyway với Spring Boot: SQL versioned, bảng flyway_schema_history, vì sao ddl-auto=update nguy hiểm production, và workflow migration an toàn cho team.
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:

Kết quả: chỉ V3 được apply. V1 và V2 bỏ qua vì đã có trong history. Đây là tính chất idempotent — và nó nằm ở phép so hai danh sách chứ không nằm trong nội dung file SQL: restart app bao nhiêu lần cũng an toàn, vì lần sau chênh lệch đã rỗng.
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: Flyway đọc lại file, tính checksum mới (-111111111), so với -987654321 trong history — lệch → FlywayException → app 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ì đây chính là kịch bản Flyway đang từ chối cho xảy ra:

Cho phép sửa file đã apply là mất tính reproducibility: hai DB có schema khác nhau trong khi Flyway history nói cả hai đều "ở version 2" — dòng version 2 không còn xác định được schema nào, 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 — cùng một bộ file, hai loại môi trường đi hai đường khác nhau:

Điều kiện kích hoạt cần nhớ: baseline-on-migrate chỉ có tác dụng khi schema không rỗng mà lại chưa có bảng history. Trên env mới (DB rỗng hoàn toàn) cờ này im lặng, và V1 vẫn chạy đầy đủ như một migration bình thường.
Sau 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:
- Acquire DB advisory lock.
- Tạo
flyway_schema_historynếu chưa tồn tại. - Scan
classpath:db/migration/lấyV*.sql, filter bỏ script đã có trong history. - Hết pending → release lock, no-op. Còn pending → apply theo thứ tự version; mỗi script apply xong ghi ngay history row (checksum,
success = true) rồi quay lại bước 4.
Ba đ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ì?
- Q2Phân biệt
flyway_schema_historyvới schema thật của app. Flyway dùng table đó để làm gì khi app restart? - Q3Team đang onboard Flyway cho app đã chạy production 1 năm (20 table, không có
flyway_schema_history). Nếu chỉ thêmflyway-coredependency và tạoV1__init_schema.sqlmà không configbaseline-on-migrate, điều gì xảy ra khi deploy? - 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 tabletaskskhông được tạo. Tại sao? Sửa thế nào? - Q5Tại sao pattern
ddl-auto: validate+ Flyway tốt hơn chỉ dùng Flyway một mình (vớiddl-auto: none)?
Bài tiếp theo: Migration patterns & CI/CD
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
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