99 Ngày Spring — Ngày 34: Flyway — version hóa schema
Từ Ngày 21 đến nay, mọi bài JPA đều đứng trên một chân co: ddl-auto để Hibernate tự vẽ bảng. Tiện cho học — và là thảm họa được hẹn giờ cho production: cột rác âm thầm, schema không ai review, ba môi trường ba phiên bản. Hôm nay ta trưởng thành hóa tầng schema bằng Flyway: mỗi thay đổi là một file SQL có version, có review, có lịch sử — schema được đối xử đúng như code. Kèm ba quy tắc viết migration sống còn và các tình huống thực tế: baseline, fail giữa chừng, backfill lớn.

Vì sao ddl-auto là thảm họa production
# application.yml quen thuộc của dev:
spring:
jpa:
hibernate:
ddl-auto: update # sửa entity → Hibernate tự sửa bảng. Tiện thật!
# Vì sao "tiện thật" là thảm họa ở production?
# 1. update chỉ THÊM — không bao giờ XÓA/ĐỔI TÊN
# đổi tên field tieuDe → title: Hibernate tạo CỘT MỚI title,
# cột tieu_de cũ (và toàn bộ dữ liệu trong đó) nằm lại thành rác — âm thầm
# 2. Không ai REVIEW được thay đổi schema
# ALTER TABLE chạy lúc app boot — không qua code review, không có diff
# 3. Không có lịch sử, không rollback
# schema hiện tại là "tổng của mọi lần boot" — không ai biết đường quay lại
# 4. Drift giữa các môi trường
# dev boot 50 lần, staging 10, production 3 → BA schema khác nhau
# "chạy ở máy em mà" phiên bản database
# 5. Nhiều instance cùng boot (scale ngang!) cùng ALTER một bảng → race
# Kết luận: schema là CODE — phải được version, review, và chạy CÓ CHỦ ĐÍCH
# ddl-auto chỉ còn một chỗ đứng ở production: validate (kiểm, không sửa)Cái bẫy lớn nhất của
update: nó chỉ thêm, không bao giờ xóa/đổi tên — refactor entity xong, cột cũ và dữ liệu trong đó nằm lại thành rác không ai hay; cộng với không review, không lịch sử, không đường lui.Drift là án tử từ từ: mỗi môi trường boot số lần khác nhau → schema khác nhau — bug "chỉ xảy ra trên staging" ra đời từ đây; và khi scale ngang (bài System Design!), nhiều instance cùng boot cùng
ALTERlà race condition tầng schema.Kết luận nền của cả bài: schema là code — phải được version, review, chạy có chủ đích;
ddl-autochỉ giữ lại một vai trò tử tế:validate.
Flyway — schema có git log của riêng nó
# Flyway — version hóa schema như version hóa code
# 1. Thêm dependency (Boot tự cấu hình phần còn lại):
# org.flywaydb:flyway-core
# (PostgreSQL cần thêm: org.flywaydb:flyway-database-postgresql)
# 2. Migration = file SQL trong src/main/resources/db/migration:
db/migration/
├── V1__tao_bang_tasks.sql
├── V2__them_cot_status.sql
├── V3__tao_bang_users_va_fk.sql
└── V4__index_tasks_created_at.sql
# Quy ước tên NGHIÊM NGẶT: V<số>__<mô_tả>.sql (HAI dấu gạch dưới!)
# V1__init.sql ✅ V1_init.sql ❌ (một gạch — Flyway lờ đi không chạy)
-- V2__them_cot_status.sql — SQL thuần, DB nào hiểu DB đó:
ALTER TABLE tasks ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'TODO';
CREATE INDEX idx_tasks_status ON tasks(status);
# 3. App boot: Flyway so bảng flyway_schema_history với thư mục migration
# → chạy những version CHƯA áp dụng, theo đúng thứ tự, ghi lại lịch sử
# flyway_schema_history: version | description | checksum | installed_on | success
# → schema giờ có git log của riêng nó — ai, cái gì, khi nào, thành công không
# 4. Và tắt vòi ddl-auto:
spring:
jpa:
hibernate:
ddl-auto: validate # entity lệch schema → FAIL NGAY lúc boot, không âm thầmMô hình gọn: mỗi thay đổi = một file
V<số>__<mô_tả>.sql— Flyway so với bảngflyway_schema_historylúc boot, chạy phần còn thiếu theo đúng thứ tự và ghi sổ: schema có lịch sử ai-cái-gì-khi-nào như git log.Chi tiết hay vấp nhất: quy ước tên hai dấu gạch dưới
V1__init.sql— một gạch là Flyway lờ đi không báo; và migration là SQL thuần của đúng DB bạn dùng — không tầng trừu tượng nào che nữa.Cặp đôi hoàn hảo: Flyway nắm schema +
ddl-auto: validatelàm trọng tài — entity lệch schema là fail ngay lúc boot với thông báo rõ ràng, thay vì âm thầm chạy sai.
Ba quy tắc viết migration
-- Ba quy tắc viết migration sống còn
-- QUY TẮC 1: migration đã chạy là BẤT BIẾN (Ngày 29 của database!)
-- Flyway lưu checksum từng file — sửa V2 đã áp dụng là app FAIL lúc boot:
-- "Migration checksum mismatch for version 2"
-- Sai ở V2? → viết V5 sửa lại. Lịch sử chỉ tiến, không viết lại.
-- QUY TẮC 2: mỗi migration NHỎ và một mục đích
-- V7__them_cot_priority.sql ✅ một việc, đọc phát hiểu
-- V8__doi_moi_thu.sql (200 dòng) ❌ hỏng giữa chừng là dở dang khó lần
-- QUY TẮC 3: tương thích lùi khi rolling deploy (code CŨ chạy chung schema MỚI)
-- Đổi tên cột tieu_de → title KHÔNG được làm một phát:
-- V9: ADD COLUMN title; copy dữ liệu; code mới ghi CẢ HAI, đọc title
-- (deploy xong, ổn định)
-- V10: DROP COLUMN tieu_de — khi không còn code nào đọc nó
-- Tên tuổi: expand → migrate → contract
-- Bonus — Repeatable migration cho view/function (chạy lại MỖI KHI file đổi):
-- R__bao_cao_task_theo_tuan.sql
CREATE OR REPLACE VIEW bao_cao_tuan AS
SELECT date_trunc('week', created_at) AS tuan, count(*) AS so_task
FROM tasks GROUP BY 1;Quy tắc 1 — bất biến (Ngày 29 phiên bản database): file đã chạy có checksum, sửa là app fail boot; sai thì viết version mới sửa lại — lịch sử chỉ tiến, không viết lại, đúng triết lý event sourcing.
Quy tắc 3 đáng giá nhất cho hệ thống thật: expand → migrate → contract — rolling deploy nghĩa là code cũ chạy chung schema mới vài phút, nên đổi tên/xóa cột phải tách hai đợt: thêm trước, dọn sau khi không ai còn đọc.
R__(repeatable) cho view/function: chạy lại mỗi khi nội dung file đổi — chỗ đứng riêng cho những object "định nghĩa lại được", khỏi đẻ version mới cho mỗi lần sửa view.
Flyway + JPA — phân vai và tình huống thực tế
# Flyway + JPA — phân vai và các tình huống thực tế
# PHÂN VAI từ hôm nay:
# Entity (@Entity, @Column) → mô tả cách CODE nhìn dữ liệu
# Migration (V*.sql) → quyết định schema THẬT
# ddl-auto: validate → trọng tài: lệch nhau là fail sớm
# Quy trình đổi schema: viết migration TRƯỚC → sửa entity khớp theo → validate gật đầu
# DB đã có sẵn bảng (dự án đang chạy)? — baseline:
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1 # coi schema hiện tại là V1, chạy từ V2 trở đi
# Migration fail giữa chừng?
# - PostgreSQL: DDL nằm trong transaction → tự rollback, sửa file, chạy lại ✅
# - MySQL: DDL KHÔNG transactional → có thể kẹt dở dang
# → flyway repair (dọn record fail) + tự dọn phần đã chạy, rồi chạy lại
# Data migration lớn (backfill hàng triệu dòng)?
# → ĐỪNG nhét vào migration boot (giữ lock, boot treo hàng phút)
# → migration chỉ đổi CẤU TRÚC; backfill chạy batch riêng ngoài giờ cao điểm
# Test migration bằng Testcontainers (Ngày 10 phần Spring... và khóa Kafka buổi 10):
# @SpringBootTest với Postgres container thật → Flyway chạy TOÀN BỘ V1→Vn từ đầu
# → CI bắt lỗi migration TRƯỚC khi nó chạm production — lưới an toàn cuối cùngPhân vai mới từ hôm nay: migration quyết định schema thật, entity chỉ mô tả cách code nhìn dữ liệu,
validatebắt lệch — quy trình đổi schema luôn là: migration trước, entity theo sau.Ba tình huống thực chiến: DB có sẵn →
baseline; fail giữa chừng → PostgreSQL tự rollback (DDL transactional) còn MySQL cầnflyway repair+ dọn tay; backfill lớn tách khỏi migration boot — migration đổi cấu trúc, dữ liệu chạy batch riêng.Lưới an toàn cuối: test migration bằng Testcontainers — CI dựng Postgres thật, Flyway chạy V1→Vn từ đầu: lỗi thứ tự, lỗi syntax, lỗi thiếu default đều lộ trước khi chạm production.
Checklist schema trưởng thành
Production:
ddl-auto: validate+ Flyway — không ngoại lệ; dev nghịch nhanh có thểupdatenhưng tốt nhất là đồng nhất luôn cho đỡ lệch thói quen.Mỗi PR đổi schema chứa: file
V*mới + entity cập nhật — reviewer đọc SQL thật, không đoán qua annotation.Đặt tên mô tả rõ:
V7__them_cot_priority_vao_tasks.sql— bảng history tự thành tài liệu.Đổi cột đang dùng: luôn expand → migrate → contract; không bao giờ
DROP/RENAMEmột phát khi đang rolling deploy.Backfill dữ liệu lớn: job riêng, ngoài giờ cao điểm, idempotent (chạy lại không hỏng — Ngày 32 System Design… à nhầm, buổi 7 khóa System Design 😉).
Bài tập nhỏ
Chuyển checkpoint Ngày 30 sang Flyway: viết
V1__tao_bang_tasks.sqltừ schema hiện có (copy từ logddl-autosinh ra), bậtvalidate, xác nhận boot sạch.Viết
V2thêm cộtprioritykèm index — rồi cố tình sửa V2 sau khi chạy để thấy lỗi checksum, và sửa đúng cách bằng V3.Thực hành expand→contract: đổi tên
mo_ta→descriptionqua 2 migration + 1 lần đổi code, mô phỏng rolling deploy.Tạo
R__thong_ke_theo_status.sql(view) — sửa nội dung và xác nhận Flyway tự chạy lại.Viết test Testcontainers dựng Postgres và boot app — đổi một migration thành SQL sai để thấy CI đỏ đúng chỗ.
Kết luận
Bốn ý gói bài: ddl-auto: update tiện dev nhưng không review, không lịch sử, tự drift — production chỉ còn validate; Flyway biến schema thành code với V__ + flyway_schema_history — git log của database; ba quy tắc: bất biến, nhỏ gọn, expand → contract cho rolling deploy; và phân vai chuẩn — migration quyết schema, entity mô tả, Testcontainers gác cổng CI. Ngày 35 ta bước xuống một tầng trừu tượng: JdbcTemplate & JdbcClient — khi nào bỏ JPA viết SQL tay, và API JdbcClient mới gọn đến bất ngờ. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


