Mã nguồn có Git. Lược đồ CSDL cũng cần thứ tương đương. Bài này về công cụ đó.

Vì sao không dùng ddl-auto: update

Nó tiện, và nó đủ tốt cho lúc học. Bốn lý do không dùng cho sản xuất:

Không xem trước được. Hibernate tự sinh ALTER TABLE lúc khởi động. Bạn không biết nó sẽ chạy gì cho tới khi nó đã chạy.

Không lùi lại được. Không có bản ghi nào về việc đã đổi gì, nên không có gì để hoàn tác.

Nó không bao giờ xoá. Bỏ một trường trong entity thì cột vẫn nằm đó mãi. Sau hai năm, lược đồ đầy cột chết mà không ai dám xoá vì không biết còn ai dùng.

Nó không làm được việc quan trọng nhất. Đổi lược đồ thật thường đi kèm chuyển dữ liệu — tách một cột thành hai, chuẩn hoá giá trị cũ. ddl-auto không biết gì về dữ liệu.

Blog này đang chạy ddl-auto: update, và CLAUDE.md của nó ghi rõ đó là nợ kỹ thuật: mọi lần cập nhật đều phải sao lưu trước, vì Hibernate sinh ALTER TABLE mà không rollback được.

Với dự án có người dùng thật, dùng công cụ migration.

Flyway

<dependency><groupId>org.flywaydb</groupId><artifactId>flyway-core</artifactId></dependency>
<dependency><groupId>org.flywaydb</groupId><artifactId>flyway-database-postgresql</artifactId></dependency>

Từ Flyway 10, mỗi loại CSDL nằm ở một artifact riêng — thiếu cái thứ hai là lỗi Unsupported Database: PostgreSQL.

Đặt tệp SQL vào src/main/resources/db/migration:

  V1__tao_bang.sql
  V2__them_cot.sql
  V3__them_chi_muc.sql

Hai dấu gạch dưới giữa số phiên bản và mô tả. Một dấu là Flyway không nhận ra tệp.

  Migrating schema "public" to version "1 - tao bang"
  Migrating schema "public" to version "2 - them cot"
  Successfully applied 2 migrations to schema "public", now at version v2

  V1   tao bang    success=t  4 ms
  V2   them cot    success=t  2 ms
  cột của san_pham: [id, ten, gia]

Flyway tự tạo bảng flyway_schema_history và ghi lại từng bước. Chạy lại lần nữa thì nó không làm gì — nó biết đã tới đâu.

Đặt ddl-auto: validate để Hibernate kiểm lược đồ khớp với entity nhưng không tự sửa:

spring:
  jpa:
    hibernate:
      ddl-auto: validate

Đây là tổ hợp tôi khuyên: Flyway đổi lược đồ, Hibernate kiểm chéo.

Checksum: sửa migration cũ là ứng dụng không lên

Tôi thêm một dòng chú thích vào V1__tao_bang.sql đã chạy:

  Migration checksum mismatch for migration version 1
  -> Applied to database : 1792983192
  -> Resolved locally    : 644015712
  Either revert the changes to the migration, or run repair to update the schema history.
  APPLICATION FAILED TO START

Chỉ là một dòng -- them mot dong chu thich. Ứng dụng từ chối khởi động.

Đây là tính năng, không phải phiền toái. Nếu Flyway cho phép sửa migration đã chạy, CSDL sản xuất sẽ khác CSDL của người mới clone dự án — cùng một lịch sử, hai lược đồ khác nhau. Kiểm checksum làm điều đó thành không thể.

Quy tắc rút ra: migration đã lên sản xuất là bất biến. Cần đổi thì viết migration mới.

Nếu bạn thật sự chắc (ví dụ chỉ sửa chú thích, trên máy dev), flyway repair cập nhật lại checksum. Đừng chạy nó trên sản xuất theo phản xạ — thông báo lỗi đang nói rằng lược đồ thật có thể khác thứ bạn nghĩ.

Migration lặp lại

  R__view_thong_ke.sql

Tệp bắt đầu bằng R__ chạy lại mỗi khi nội dung đổi. Dùng cho view, function, stored procedure — những thứ định nghĩa bằng CREATE OR REPLACE nên chạy lại được.

Nó giữ định nghĩa view trong một tệp duy nhất thay vì rải qua mười migration.

Đổi lược đồ không dừng dịch vụ

Trong lúc triển khai, phiên bản cũ và mới của ứng dụng chạy cùng lúc. Nên migration phải tương thích với cả hai.

Ba loại thay đổi:

Thêm cột — an toàn, miễn là có DEFAULT hoặc cho phép null:

alter table san_pham add column gia integer not null default 0;

NOT NULL không có DEFAULT trên bảng đã có dữ liệu thì PostgreSQL từ chối. Đây là lỗi hay gặp nhất, và với JPA thì khai @ColumnDefault("0") để Hibernate sinh DEFAULT trong câu ALTER TABLE.

Bỏ cột — làm ba bước, qua ba lần triển khai:

  1. Triển khai mã không còn dùng cột đó
  2. Migration: alter table ... drop column

Làm ngược lại là phiên bản cũ đang chạy nổ ngay khi migration chạy xong.

Đổi tên cột — bốn bước, và đây là chỗ hay bị làm tắt:

  1. Thêm cột mới, migration chép dữ liệu sang
  2. Triển khai mã ghi vào CẢ HAI cột, đọc cột mới
  3. Triển khai mã chỉ dùng cột mới
  4. Migration bỏ cột cũ

Dài, nhưng mỗi bước đều lùi lại được.

Thêm chỉ mục trên bảng lớn — PostgreSQL khoá bảng suốt quá trình. Dùng:

create index concurrently idx_san_pham_ten on san_pham(ten);

Chú ý CONCURRENTLY không chạy được trong giao dịch, nên Flyway cần migration đó ở chế độ không giao dịch — đặt tên tệp kèm cấu hình flyway.executeInTransaction=false.

Vài chi tiết vận hành

Không bao giờ sửa dữ liệu bằng tay trên sản xuất. Viết migration, kể cả cho một câu UPDATE. Nó được kiểm duyệt, được lưu vết, và chạy giống nhau ở mọi môi trường.

Migration phải chạy được nhiều lần ở mức an toàn — dùng if not exists khi hợp lý. Một migration hỏng giữa chừng phải sửa được mà không cần dựng lại CSDL.

Chạy migration tách khỏi ứng dụng khi có nhiều bản sao. Nếu năm container cùng khởi động, cả năm cùng chạy Flyway. Flyway có khoá nên chỉ một cái thắng, nhưng bốn cái kia chờ — và với migration dài, chúng hết hạn kiểm tra sức khoẻ rồi bị khởi động lại. Chạy migration ở một bước riêng trước khi triển khai.

Sao lưu trước mọi migration. Không thương lượng.

Flyway hay Liquibase

Flyway: SQL thuần, đơn giản, dễ đọc. Bạn viết đúng câu lệnh sẽ chạy.

Liquibase: mô tả bằng XML/YAML/JSON, độc lập với loại CSDL, có rollback khai báo được. Mạnh hơn, phức tạp hơn.

Với dự án dùng một CSDL — và đó là gần như mọi dự án — tôi chọn Flyway. Tính "độc lập CSDL" của Liquibase hiếm khi được dùng thật, mà lớp trừu tượng thì luôn phải trả tiền.

Thử ba mươi giây

grep -rn "ddl-auto" src/main/resources/

Thấy update trong cấu hình sản xuất nghĩa là lược đồ CSDL của bạn đang được sinh tự động, không xem trước được, không lùi lại được. Nếu chưa đổi được ngay thì ít nhất hãy kiểm log sau mỗi lần triển khai:

docker compose logs blog | grep -i "CommandAcceptanceException"

Ngày mai: pool kết nối — và vì sao ba trong tám luồng bị từ chối thẳng.