Spring REST API & Data JPA/Flyway Migration Spring Boot — version schema như code
43/46
Bài 43 / 46~14 phútMigration & CapstoneMiễn phí lượt xem

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ạnHệ quả
Không versionKhông biết DB đang ở "phiên bản" nào — không audit trail
Không drop / renameSchema bẩn — cột cũ tồn tại vĩnh viễn
Race condition2 pod startup đồng thời → conflict ALTER TABLE
Không reproducibleSchema phụ thuộc Hibernate version + entity scan order

Migration tool giải quyết bằng 3 nguyên tắc:

  1. Versioned — mỗi change có số version, apply đúng 1 lần, theo thứ tự.
  2. Reproducible — SQL script là source of truth, chạy trên bất kỳ env nào cho cùng kết quả.
  3. Audited — lịch sử mọi thay đổi lưu trong DB, có timestamp và checksum.
App startup
App startup
Flyway scan
Flyway scan
Schema updated
Schema updated

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
Hai gạch dưới — không phải một

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:

PrefixLoạiChạy khi nào
V<version>__<desc>.sqlVersionedMột lần, theo thứ tự version
R__<desc>.sqlRepeatableKhi checksum file thay đổi
U<version>__<desc>.sqlUndoRollback 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.
  • successtrue nếu apply thành công, false nế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:

Ba file migration trên đĩa đối chiếu với hai row trong bảng flyway_schema_history: V1 và V2 đã có row nên bị bỏ qua, còn V3 chưa có row nào nên là chênh lệch duy nhất và được apply

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:

Cùng một file V2 nhưng env A apply bản gốc còn env B apply bản đã sửa: hai môi trường ghi hai checksum khác nhau và có schema khác nhau, trong khi history của cả hai đều nói version bằng 2

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í.

Boot autoconfigure

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:

  1. Flyway tạo flyway_schema_history (nếu chưa có).
  2. Phát hiện V1__init_schema.sql chưa có trong history.
  3. Chạy script, tạo table projects và 2 index.
  4. 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 DEFAULT value để 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:

Cùng một bộ file V1 baseline và V2: trên production schema không rỗng và chưa có bảng history nên baseline kích hoạt, chèn row BASELINE version 1 rồi skip V1 và apply V2; trên môi trường mới DB rỗng nên baseline không kích hoạt và V1 chạy đầy đủ trước V2; hai đường về cùng một schema cuối

Đ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
V1 cho new env phai idempotent

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:

  1. Acquire DB advisory lock.
  2. Tạo flyway_schema_history nếu chưa tồn tại.
  3. Scan classpath:db/migration/ lấy V*.sql, filter bỏ script đã có trong history.
  4. 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:

  1. Lock trước, apply sau — đảm bảo concurrent-safe dù 100 pod khởi động đồng thời.
  2. Filter theo history — chỉ apply pending, idempotent với restart.
  3. Checksum ghi sau khi apply thành công — nếu script fail, success = false trong history, app không start, DBA có thể diagnose rồi chạy flyway 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

Tài liệu chính chủ

Flyway:

Spring Boot:

ddl-auto deep dive:

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=update không đủ production-grade và ddl-auto=none/validate là 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=update khô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ào flyway_schema_history.
  • Checksum bất biến sau khi apply — sửa file đã apply gây FlywayException startup 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-postgresql dependency. 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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì 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ì?
  2. Q2
    Phân biệt flyway_schema_history với schema thật của app. Flyway dùng table đó để làm gì khi app restart?
  3. Q3
    Team đ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?
  4. Q4
    Developer 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?
  5. Q5
    Tại sao pattern ddl-auto: validate + Flyway tốt hơn chỉ dùng Flyway một mình (với ddl-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

Đặt 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

Bài tiếp theo

Flyway patterns & CI/CD — forward-only, multi-env, repeatable, zero-downtime