Bài 25 tạo table products bằng schema.sql, file mà Spring Boot chỉ chạy trên database embedded, còn bài 26 và 28 để Hibernate sinh mọi table qua spring.jpa.hibernate.ddl-auto. Cả hai cách đều dựng schema từ con số không. Không cách nào biết một database đang chạy đã nhận những thay đổi nào, và cũng không cách nào ghi lại rằng một thay đổi đã xảy ra. Bài này thay cả hai bằng Flyway: các file SQL đánh số nằm trong repository, chạy đúng một lần trên mỗi database, theo thứ tự, và được ghi vào một table.
Các ví dụ dùng Spring Boot 4.1.1 và Java 21 với PostgreSQL 18 chạy trong Docker. H2 xuất hiện ở những phần có ghi rõ, MySQL 8.4 trong một phép so sánh, và Liquibase ở cuối bài. Application chạy ở port 8131 thay vì 8080 mặc định, và đường dẫn dài trong output được rút gọn thành /…/.
![]()
Phần đầu bắt đầu từ một catalogue có các table do ddl-auto=update tạo và quan sát nó làm gì với các row đã có sẵn. Phần còn lại dựng schema của bài 28 bằng Flyway và chạy thử từng lỗi mà một công cụ migration hay gặp.
Vì sao không dùng ddl-auto ở production
Database chạy trong container, giống bài 25:
docker run -d --name sb-a31-pg -e POSTGRES_USER=shop -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=shop -p 55431:5432 postgres:18Profile postgres giữ thông tin kết nối. Dòng cuối của nó thuộc về phần nói về Hibernate và Flyway; các lần chạy trong phần này ghi đè nó trên command line:
spring.datasource.url=jdbc:postgresql://localhost:55431/shop
spring.datasource.username=shop
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=validateCác entity là Category, Tag, Product, Order và OrderLine của bài 28, bỏ các table customer, và Order chỉ còn id cùng các line. Product giữ các relationship của bài 28 và lấy kích thước column từ bài 26:
@Column(nullable = false, length = 120)
private String name;
@Column(nullable = false, unique = true, length = 40)
private String sku;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private int stock;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
@ManyToMany
@JoinTable(name = "product_tags",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();Cho phần này, database thứ hai trong container, ddlauto, nhận các table từ update với Flyway bị tắt:
docker exec sb-a31-pg psql -U shop -d shop -c 'create database ddlauto;'java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8131 --spring.profiles.active=postgres --spring.datasource.url=jdbc:postgresql://localhost:55431/ddlauto --spring.flyway.enabled=false --spring.jpa.hibernate.ddl-auto=update --logging.level.org.hibernate.SQL=DEBUG --spring.main.web-application-type=none--spring.main.web-application-type=none khiến process thoát ngay khi khởi động xong; mọi lần chạy trong bài đều dùng nó. update tạo sáu table, và psql thêm ba category cùng ba product: KB-01 giá 89.90, MS-01 giá 24.50 và HUB-07 giá 39.00. Hai bản sao của database đó, tạo bằng create database … template ddlauto, phục vụ lần đổi tên thứ hai bên dưới và phần nói về database có sẵn.
Đổi tên field với ddl-auto=update
Thay đổi đầu tiên đổi name thành title, từ mà các cửa hàng online hay dùng cho sản phẩm; parameter của constructor và getter đổi theo:
@Column(nullable = false, length = 120)
private String name;
private String title; Khởi động với update, application ghi log như sau. Lần chạy này cũng mang theo thay đổi SKU của phần kế tiếp, dòng log của nó nằm ở đó:
2026-09-13T17:32:28.040+07:00 DEBUG 42679 --- [demo] [ main] org.hibernate.SQL : alter table if exists products add column title varchar(120) not null
2026-09-13T17:32:28.041+07:00 WARN 42679 --- [demo] [ main] o.h.t.s.i.ExceptionHandlerLoggedImpl : GenerationTarget encountered exception accepting command : Error executing DDL "alter table if exists products add column title varchar(120) not null" via JDBC [ERROR: column "title" of relation "products" contains null values]
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table if exists products add column title varchar(120) not null" via JDBC [ERROR: column "title" of relation "products" contains null values]
...
Caused by: org.postgresql.util.PSQLException: ERROR: column "title" of relation "products" contains null values
...
2026-09-13T17:32:28.057+07:00 INFO 42679 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-09-13T17:32:28.134+07:00 INFO 42679 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.382 seconds (process running for 1.579)PostgreSQL từ chối thêm một column NOT NULL vào table đã có row. Hibernate chỉ ghi lời từ chối đó thành một WARN, và application vẫn khởi động, với một mapping trỏ tới column mà table không có:
docker exec sb-a31-pg psql -U shop -d ddlauto -c 'select id, name, title, sku from products order by id'ERROR: column "title" does not exist
LINE 1: select id, name, title, sku from products order by id
^Bỏ nullable = false khỏi field đã đổi tên, cùng lần khởi động đó trên bản sao ddlauto2 đi được xa hơn:
@Column(nullable = false, length = 120)
@Column(length = 120)
private String title;2026-09-13T17:34:42.096+07:00 DEBUG 43295 --- [demo] [ main] org.hibernate.SQL : alter table if exists products add column title varchar(120)docker exec sb-a31-pg psql -U shop -d ddlauto2 -c 'select id, name, title, sku, price from products order by id' id | name | title | sku | price
----+---------------------+-------+--------+-------
1 | Mechanical keyboard | | KB-01 | 90
2 | Wireless mouse | | MS-01 | 25
3 | USB-C hub | | HUB-07 | 39
(3 rows)Ba cái tên vẫn nằm trong name, column không còn field nào map tới, còn title tồn tại nhưng trống trơn; column price thuộc về phần kế tiếp. Column cũ còn giữ NOT NULL, nên một câu INSERT với đúng các column mà entity đang map, gửi từ psql, bị từ chối:
docker exec sb-a31-pg psql -U shop -d ddlauto2 -c "insert into products (title, sku, price, stock, category_id) values ('Webcam', 'CAM-01', 59, 4, 1)"ERROR: null value in column "name" of relation "products" violates not-null constraint
DETAIL: Failing row contains (4, null, 59, CAM-01, 4, 1, Webcam).update so mapping với table rồi thêm những gì còn thiếu. Không có gì trong entity cho biết title từng là name, nên một lần đổi tên thành một column mới, và data không bao giờ được chuyển sang.
Đổi độ dài hoặc scale của column với update
Mỗi lần chạy trong hai lần trên còn mang thêm một thay đổi. Lần thứ nhất nới rộng SKU:
@Column(nullable = false, unique = true, length = 40)
@Column(nullable = false, unique = true, length = 64)
private String sku;2026-09-13T17:32:28.036+07:00 DEBUG 42679 --- [demo] [ main] org.hibernate.SQL : alter table if exists products alter column sku set data type varchar(64)Lần thứ hai còn bỏ scale = 2 khỏi price, kiểu thay đổi dễ lọt vào khi dọn lại annotation. scale mặc định của @Column là 0:
@Column(nullable = false, precision = 10, scale = 2)
@Column(nullable = false, precision = 10)
private BigDecimal price;2026-09-13T17:34:42.090+07:00 DEBUG 43295 --- [demo] [ main] org.hibernate.SQL : alter table if exists products alter column price set data type numeric(10,0)
2026-09-13T17:34:42.095+07:00 DEBUG 43295 --- [demo] [ main] org.hibernate.SQL : alter table if exists products alter column sku set data type varchar(64)Vậy update của Hibernate 7.4 có sửa column đã tồn tại. SKU rộng hơn thì vô hại; scale thì không. PostgreSQL chuyển các giá trị đang lưu sang numeric(10,0), và kết quả nằm trong output psql ở trên: 89.90 thành 90, 24.50 thành 25. Log của lần chạy đó không có một dòng WARN hay ERROR nào.
create, create-drop và việc không có lịch sử
Hai giá trị còn lại có đụng tới schema còn thô bạo hơn trên một database có data. create drop mọi table được map ở mỗi lần khởi động, như câu drop table if exists products cascade trong bài 26, nên mỗi lần restart đều bắt đầu với table rỗng. create-drop làm y như vậy và drop thêm lần nữa khi tắt application.
Và không giá trị nào để lại dấu vết. Không có gì ghi lại câu lệnh nào đã chạy trên database nào. DDL được sinh lúc khởi động từ những class tình cờ được deploy, nên không ai review nó trước khi chạy. Kết quả còn phụ thuộc vào con đường mà database đã đi qua: update đặt cho unique constraint của SKU trong ddlauto một cái tên sinh tự động, ukfhmd06dsmj6k0n90swsh8ie9g, trong khi create của bài 26 tạo ra products_sku_key cho cùng mapping đó.
Cùng lần đổi tên, viết thành một versioned migration mà một phần sau sẽ chạy trên đúng ba product đó:

Thêm Flyway vào project Spring Boot 4.1.1
Id trên Spring Initializr là flyway. Project của bài này được sinh bằng:
curl -s "https://start.spring.io/starter.zip?type=gradle-project&language=java&bootVersion=4.1.1&javaVersion=21&groupId=com.example&artifactId=demo&name=demo&packageName=com.example.demo&dependencies=web,validation,data-jpa,h2,postgresql,flyway" -o demo.zipSo với build của bài 26, có ba dòng mới:
dependencies {
implementation 'org.springframework.boot:spring-boot-h2console'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.flywaydb:flyway-database-postgresql'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-flyway-test'
testImplementation 'org.springframework.boot:spring-boot-starter-validation-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway-test</artifactId>
<scope>test</scope>
</dependency>Starter này mang theo phần hỗ trợ Flyway của Boot, trong Boot 4 là một module riêng giống phần JPA ở bài 26, và chính Flyway:
./gradlew dependencies --configuration runtimeClasspathRút gọn còn các dòng liên quan tới Flyway:
+--- org.springframework.boot:spring-boot-starter-flyway -> 4.1.1
| +--- org.springframework.boot:spring-boot-starter:4.1.1 (*)
| +--- org.springframework.boot:spring-boot-starter-jdbc:4.1.1 (*)
| +--- org.springframework.boot:spring-boot-flyway:4.1.1
| | +--- org.springframework.boot:spring-boot:4.1.1 (*)
| | +--- org.springframework.boot:spring-boot-jdbc:4.1.1 (*)
| | \--- org.flywaydb:flyway-core:12.4.0
| | \--- tools.jackson.core:jackson-databind:3.1.1 -> 3.1.5
| \--- org.springframework.boot:spring-boot-jdbc:4.1.1 (*)
+--- org.flywaydb:flyway-database-postgresql -> 12.4.0
| \--- org.flywaydb:flyway-core:12.4.0 (*)flyway-core12.4.0 là engine. Bản thân nó có sẵn phần hỗ trợ cho một số database: các class của H2 nằm trong nó, dướiorg/flywaydb/core/internal/database/h2/, còn của PostgreSQL thì không.flyway-database-postgresqlchứaPostgreSQLDatabaseTypevà, trong cùng jar, phần hỗ trợ CockroachDB. Initializr thêm nó vì PostgreSQL được chọn; với MySQL thì nó thêmorg.flywaydb:flyway-mysql.spring-boot-starter-flyway-testlà bản-testmà starter nào của Boot 4 cũng có.
Khởi động khi thiếu flyway-database-postgresql
Bỏ đúng dependency đó đi, jar vẫn build được, và thất bại lúc khởi động trên PostgreSQL:
2026-09-13T17:29:10.133+07:00 ERROR 42175 --- [demo] [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.class]: Failed to initialize dependency 'flywayInitializer' of LoadTimeWeaverAware bean 'entityManagerFactory': Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration$FlywayConfiguration.class]: Unsupported Database: PostgreSQL 18.6
...
Caused by: org.flywaydb.core.api.FlywayException: Unsupported Database: PostgreSQL 18.6Lúc build không có gì báo lỗi, vì không dòng code nào tham chiếu tới module này. Thông báo nêu tên database và version nhưng không nhắc tới dependency bị thiếu, trong khi PostgreSQL 18.6 hoàn toàn được hỗ trợ: có module trở lại, lần chạy ở phần sau đi qua mà không có một warning nào về nó.
Location mặc định và các thiết lập của Flyway
spring.flyway.locations không có giá trị mặc định trong property metadata của Boot. Giá trị được gán trong constructor của FlywayProperties, trong spring-boot-flyway-4.1.1.jar:
javap -c -p -classpath spring-boot-flyway-4.1.1.jar org.springframework.boot.flyway.autoconfigure.FlywayProperties 14: ldc #15 // String classpath:db/migrationVậy Flyway đọc src/main/resources/db/migration, thư mục mà Initializr tạo sẵn, để trống. Cùng constructor đó đặt các quy ước đặt tên mà những phần sau dựa vào: prefix V, dấu phân cách __, đuôi .sql, prefix R cho repeatable migration và tên table flyway_schema_history. Các thiết lập bài này đụng tới, cùng giá trị mặc định trong 4.1.1:
| Property | Mặc định | Các lần chạy cho thấy |
|---|---|---|
spring.flyway.enabled | true | Flyway chạy ở mọi lần khởi động khi có trên classpath |
spring.flyway.locations | classpath:db/migration | đặt trong FlywayProperties, không nằm trong metadata |
spring.flyway.validate-on-migrate | true | một migration bị sửa sẽ chặn khởi động |
spring.flyway.validate-migration-naming | false | file đặt sai tên bị bỏ qua, chỉ có một dòng INFO |
spring.flyway.out-of-order | false | một version thấp hơn version hiện tại làm validate thất bại |
spring.flyway.baseline-on-migrate | false | schema có dữ liệu mà không có history table bị từ chối |
spring.flyway.baseline-version | 1 | version mà row baseline ghi nhận |
spring.flyway.clean-disabled | true | clean() ném exception |
Viết versioned migration cho schema của catalogue
Các file nằm ở location mặc định:
src/main/resources
├── application.properties
├── application-postgres.properties
└── db
└── migration
├── V1__create_catalog.sql
└── V2__create_orders.sqlV1 tạo các table của catalogue:
CREATE TABLE categories (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(60) NOT NULL,
CONSTRAINT uk_categories_name UNIQUE (name)
);
CREATE TABLE tags (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(40) NOT NULL,
CONSTRAINT uk_tags_name UNIQUE (name)
);
CREATE TABLE products (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(120) NOT NULL,
sku VARCHAR(40) NOT NULL,
price NUMERIC(10, 2) NOT NULL,
stock INTEGER NOT NULL,
category_id BIGINT NOT NULL,
CONSTRAINT uk_products_sku UNIQUE (sku),
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES categories (id)
);
CREATE INDEX idx_products_category_id ON products (category_id);
CREATE TABLE product_tags (
product_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
CONSTRAINT pk_product_tags PRIMARY KEY (product_id, tag_id),
CONSTRAINT fk_product_tags_product FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT fk_product_tags_tag FOREIGN KEY (tag_id) REFERENCES tags (id)
);V2 tạo order, với các line tham chiếu tới product:
CREATE TABLE orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
);
CREATE TABLE order_lines (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(10, 2) NOT NULL,
CONSTRAINT fk_order_lines_order FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_order_lines_product FOREIGN KEY (product_id) REFERENCES products (id)
);
CREATE INDEX idx_order_lines_order_id ON order_lines (order_id);
CREATE INDEX idx_order_lines_product_id ON order_lines (product_id);Đoạn SQL này cho Hibernate đúng thứ mà các entity cần, cộng thêm những thứ Hibernate sẽ không tự viết:
- Tên khớp với mapping.
products,categoriesvàorder_lineslấy từ@Table,category_idtừ@JoinColumn,product_tagscùngproduct_idvàtag_idtừ@JoinTable, cònunit_pricetừ fieldunitPrice, được naming strategy của Boot đổi sang snake case như DDL của bài 28 đã cho thấy. BIGINT GENERATED BY DEFAULT AS IDENTITYlà column màGenerationType.IDENTITYcần, đúng định nghĩa Hibernate sinh ra ở bài 26.- Constraint có tên do mình chọn,
uk_products_skuvàfk_products_categorythay vìUKfhmd06dsmj6k0n90swsh8ie9g, để migration sau có thể gọi tới. - Column foreign key có index. PostgreSQL không tự tạo index cho foreign key, và các table do
updatedựng không có index nào, như phần về database có sẵn sẽ cho thấy. product_tagscó primary key gồm hai column, đúng thứ Hibernate sinh cho mộtSetở bài 28.
Lần chạy đầu tiên: Flyway ghi gì vào log lúc khởi động
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8131 --spring.profiles.active=postgres --spring.main.web-application-type=noneTrên database shop còn trống, từ connection đầu tiên của pool tới cuối quá trình khởi động, bỏ khối thông tin database của Hibernate:
2026-09-13T17:29:12.738+07:00 INFO 42180 --- [demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-09-13T17:29:12.747+07:00 INFO 42180 --- [demo] [ main] org.flywaydb.core.FlywayExecutor : Database: jdbc:postgresql://localhost:55431/shop (PostgreSQL 18.6)
2026-09-13T17:29:12.774+07:00 INFO 42180 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Schema history table "public"."flyway_schema_history" does not exist yet
2026-09-13T17:29:12.775+07:00 INFO 42180 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.007s)
2026-09-13T17:29:12.785+07:00 INFO 42180 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table "public"."flyway_schema_history" ...
2026-09-13T17:29:12.821+07:00 INFO 42180 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": << Empty Schema >>
2026-09-13T17:29:12.825+07:00 INFO 42180 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "1 - create catalog"
2026-09-13T17:29:12.841+07:00 INFO 42180 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "2 - create orders"
2026-09-13T17:29:12.850+07:00 INFO 42180 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.012s)
2026-09-13T17:29:12.888+07:00 INFO 42180 --- [demo] [ main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
2026-09-13T17:29:12.915+07:00 INFO 42180 --- [demo] [ main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.4.5.Final
...
2026-09-13T17:29:13.547+07:00 INFO 42180 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-09-13T17:29:13.614+07:00 INFO 42180 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.465 seconds (process running for 1.664)Database:nêu URL và version của server. Flyway 12.4.0 không nói gì thêm về PostgreSQL 18.6; trên H2 thì nó có in một warning, trích ở phần về Hibernate và Flyway.- History table chưa tồn tại, nên Flyway tạo nó, báo
<< Empty Schema >>rồi mới migrate. - Mỗi migration có một dòng log, gồm version và phần mô tả lấy từ tên file, dấu gạch dưới đổi thành khoảng trắng.
- Hibernate khởi động sau khi Flyway xong việc:
HHH008540vàEntityManagerFactoryđứng sauSuccessfully applied 2 migrations, vàddl-auto=validatetìm thấy đúng các table nó cần. - Version của chính Flyway không xuất hiện ở đâu trong log; muốn biết thì đọc dependency tree.
flyway_schema_history sau lần chạy đầu tiên
docker exec sb-a31-pg psql -U shop -d shop -c 'select installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success from flyway_schema_history order by installed_rank' installed_rank | version | description | type | script | checksum | installed_by | installed_on | execution_time | success
----------------+---------+----------------+------+------------------------+-----------+--------------+----------------------------+----------------+---------
1 | 1 | create catalog | SQL | V1__create_catalog.sql | 18327164 | shop | 2026-09-13 17:29:12.815284 | 8 | t
2 | 2 | create orders | SQL | V2__create_orders.sql | 678017735 | shop | 2026-09-13 17:29:12.835719 | 4 | t
(2 rows)Mỗi migration một row. checksum được tính từ nội dung file và chính là thứ mà phần sửa migration đã chạy sẽ đụng phải, installed_by là user của database, execution_time tính bằng mili giây. Sau lần chạy này, psql thêm vào shop đúng ba category và ba product như trong ddlauto.
Quy tắc đặt tên: prefix, version, hai dấu gạch dưới
Một versioned migration được đặt tên gồm V, version, hai dấu gạch dưới, phần mô tả và .sql. Các phần của version cách nhau bằng một dấu gạch dưới, nên V1_1 là version 1.1. Để xem Flyway sắp thứ tự version ra sao và xử lý thế nào với tên sai quy tắc, một database riêng tên naming nhận một thư mục gồm các migration một dòng, truyền vào bằng --spring.flyway.locations=filesystem:/…/naming:
naming
├── V1__create_step.sql
├── V1_1__one_point_one.sql
├── V2__two.sql
├── V9__nine.sql
├── V10__ten.sql
├── V11_single_underscore.sql
├── V13__wrong_suffix.txt
└── v12__lowercase_prefix.sqlLog lúc khởi động:
2026-09-13T17:29:58.281+07:00 INFO 42246 --- [demo] [ main] o.f.c.i.resource.ResourceNameValidator : 2 SQL migrations were detected but not run because they did not follow the filename convention.
2026-09-13T17:29:58.281+07:00 INFO 42246 --- [demo] [ main] o.f.c.i.resource.ResourceNameValidator : Set 'validateMigrationNaming' to true to fail fast and see a list of the invalid file names.
...
2026-09-13T17:29:58.361+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "1 - create step"
2026-09-13T17:29:58.376+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "1.1 - one point one"
2026-09-13T17:29:58.384+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "2 - two"
2026-09-13T17:29:58.391+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "9 - nine"
2026-09-13T17:29:58.396+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "10 - ten"
2026-09-13T17:29:58.400+07:00 INFO 42246 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 5 migrations to schema "public", now at version v10 (execution time 00:00.011s)- Version được so như số.
10chạy sau9,1.1nằm giữa1và2, dùlsliệt kêV10__ten.sqlđầu tiên. - Hai file bị bỏ qua chỉ với log INFO, và application vẫn khởi động mà không có chúng. Con số là 2 chứ không phải 3:
V13__wrong_suffix.txtkhông có đuôi.sql, nên Flyway không coi nó là SQL migration.
Với spring.flyway.validate-migration-naming=true, cùng thư mục đó chặn khởi động và nêu tên cả hai file:
Caused by: org.flywaydb.core.api.FlywayException: Invalid SQL filenames found:
Invalid versioned migration name format: V11_single_underscore.sql (could not recognise version number 11_single_underscore)
Unrecognised migration name format: v12__lowercase_prefix.sqlMột migration lặng lẽ không bao giờ chạy còn tệ hơn một lần khởi động thất bại, nên thiết lập này đáng bật:
spring.flyway.validate-migration-naming=truespring:
flyway:
validate-migration-naming: trueSửa một migration đã chạy
V1 đã chạy trên shop. Giả sử SKU cần 64 ký tự, và thay đổi được sửa ngay chỗ định nghĩa column, cùng với length = 64 trên entity:
CREATE TABLE products (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(120) NOT NULL,
sku VARCHAR(40) NOT NULL,
sku VARCHAR(64) NOT NULL,
price NUMERIC(10, 2) NOT NULL,Lần khởi động tiếp theo:
2026-09-13T17:31:25.525+07:00 ERROR 42537 --- [demo] [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.class]: Failed to initialize dependency 'flywayInitializer' of LoadTimeWeaverAware bean 'entityManagerFactory': Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration$FlywayConfiguration.class]: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 1
-> Applied to database : 18327164
-> Resolved locally : 124720333
Either revert the changes to the migration, or run repair to update the schema history.
Need more flexibility with validation rules? Learn more: https://help.red-gate.com/help/flyway-cli12/help_4.aspx?topic=flyway-blog/older-posts/customize-validation-rules-with-ignoremigrationpatterns
...
Caused by: org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validationvalidate-on-migrate là true, nên Flyway so từng migration đã chạy với file của nó trước khi chạy bất cứ thứ gì. 18327164 là checksum được ghi khi V1 chạy, 124720333 là checksum của file đã sửa. Không có bước kiểm tra này, thay đổi sẽ không làm gì với shop, nơi V1 không bao giờ chạy lại, mà chỉ tới được các database tạo từ nay về sau, và những database đó sẽ có column sku khác với production.
Thông báo đưa ra hai lối thoát. repair sẽ ghi 124720333 vào history table và che đi sự khác biệt trong khi shop vẫn giữ VARCHAR(40). Lối còn lại mới là đúng ở đây.
Cách sửa đúng là một migration mới
V1 trở về đúng như lúc đã chạy, và thay đổi trở thành version kế tiếp:
ALTER TABLE products ALTER COLUMN sku TYPE VARCHAR(64);2026-09-13T17:31:30.632+07:00 INFO 42568 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 3 migrations (execution time 00:00.011s)
2026-09-13T17:31:30.657+07:00 INFO 42568 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 2
2026-09-13T17:31:30.662+07:00 INFO 42568 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "3 - widen product sku"
2026-09-13T17:31:30.675+07:00 INFO 42568 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "public", now at version v3 (execution time 00:00.006s)
2026-09-13T17:31:31.355+07:00 INFO 42568 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'docker exec sb-a31-pg psql -U shop -d shop -c '\d products' -c 'select installed_rank, version, description, script, checksum, success from flyway_schema_history order by installed_rank' Table "public.products"
Column | Type | Collation | Nullable | Default
-------------+------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
name | character varying(120) | | not null |
sku | character varying(64) | | not null |
price | numeric(10,2) | | not null |
stock | integer | | not null |
category_id | bigint | | not null |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
"idx_products_category_id" btree (category_id)
"uk_products_sku" UNIQUE CONSTRAINT, btree (sku)
Foreign-key constraints:
"fk_products_category" FOREIGN KEY (category_id) REFERENCES categories(id)
Referenced by:
TABLE "order_lines" CONSTRAINT "fk_order_lines_product" FOREIGN KEY (product_id) REFERENCES products(id)
TABLE "product_tags" CONSTRAINT "fk_product_tags_product" FOREIGN KEY (product_id) REFERENCES products(id)
installed_rank | version | description | script | checksum | success
----------------+---------+-------------------+---------------------------+------------+---------
1 | 1 | create catalog | V1__create_catalog.sql | 18327164 | t
2 | 2 | create orders | V2__create_orders.sql | 678017735 | t
3 | 3 | widen product sku | V3__widen_product_sku.sql | 1537025089 | t
(3 rows)shop có column rộng hơn, V1 vẫn giữ checksum ban đầu, và một database tạo ngày mai nhận đúng column đó qua đúng hai file này. Tên constraint và index là những cái tên V1 đã chọn.

Flyway và Hibernate chia nhau quá trình khởi động thế nào
ddl-auto mặc định là none khi Flyway quản lý database
Bài 26 đọc trong HibernateDefaultDdlAutoProvider rằng Boot đặt create-drop cho database embedded, trừ khi có một công cụ quản lý schema như Flyway đảm nhận nó. Một runner in ra giá trị Hibernate nhận được, trên H2 in-memory không có thiết lập datasource nào:
package com.example.demo.lab;
import java.sql.Connection;
import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("inspect")
class DdlAutoInspector implements CommandLineRunner {
private final EntityManagerFactory entityManagerFactory;
private final DataSource dataSource;
private final ApplicationContext context;
DdlAutoInspector(EntityManagerFactory entityManagerFactory, DataSource dataSource, ApplicationContext context) {
this.entityManagerFactory = entityManagerFactory;
this.dataSource = dataSource;
this.context = context;
}
@Override
public void run(String... args) throws Exception {
System.out.println("hibernate.hbm2ddl.auto = " + entityManagerFactory.getProperties().get("hibernate.hbm2ddl.auto"));
System.out.println("Flyway beans = " + java.util.Arrays.toString(context.getBeanNamesForType(org.flywaydb.core.Flyway.class)));
try (Connection connection = dataSource.getConnection()) {
var meta = connection.getMetaData();
System.out.println("database = " + meta.getDatabaseProductName() + " " + meta.getDatabaseProductVersion() + " at " + meta.getURL());
}
}
}java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8131 --spring.profiles.active=inspect --spring.sql.init.mode=never --spring.main.web-application-type=nonehibernate.hbm2ddl.auto = null
Flyway beans = [flyway]
database = H2 2.4.240 (2025-09-22) at jdbc:h2:mem:8e4df5a9-a2c3-4186-b038-3d4ccac95aecCùng câu lệnh đó, thêm --spring.flyway.enabled=false:
hibernate.hbm2ddl.auto = create-drop
Flyway beans = []
database = H2 2.4.240 (2025-09-22) at jdbc:h2:mem:2601e865-d175-4e70-8110-9f3b73a9097dnull nghĩa là none: Hibernate để schema cho Flyway, thứ vừa dựng nó từ V1 và V2 ngay trước đó. Điều quyết định là Flyway có được bật hay không, chứ không phải jar của nó có mặt hay không: khi tắt Flyway, jar vẫn còn đó và create-drop quay lại. Trên H2, Flyway 12.4.0 còn in ra warning mà nó chưa từng in cho PostgreSQL 18.6:
2026-09-13T17:29:53.712+07:00 WARN 42241 --- [demo] [ main] o.f.c.internal.database.base.Database : Using H2 2.4.240 which is newer than the version Flyway has been verified with. The latest verified version of H2 is 2.3.232.--spring.sql.init.mode=never giữ data.sql, chủ đề của phần cuối mục này, ra khỏi lần chạy trên.
Flyway chạy trước EntityManagerFactory
Log của lần chạy đầu tiên đặt mọi dòng của Flyway trước HHH008540, và thứ tự đó được khai báo chứ không phải tình cờ. Mọi lần khởi động thất bại vì Flyway trong bài đều bắt đầu bằng cùng một chuỗi: Failed to initialize dependency 'flywayInitializer' of LoadTimeWeaverAware bean 'entityManagerFactory'. Boot tạo flywayInitializer, bean chạy migration, trước khi dựng EntityManagerFactory, nên một migration thất bại đồng nghĩa với việc Hibernate không bao giờ khởi động. Toàn bộ trình tự, theo log trên shop khi V3 đang chờ chạy:

ddl-auto=validate so với schema đã migrate
Với spring.jpa.hibernate.ddl-auto=validate trong profile postgres, Hibernate so mapping với các table Flyway đã dựng. Sau V3, việc đổi name thành title ở phần đầu được đưa vào entity mà không có migration:
2026-09-13T17:32:25.824+07:00 INFO 42677 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Schema "public" is up to date. No migration necessary.
2026-09-13T17:32:26.513+07:00 ERROR 42677 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Failed to initialize JPA EntityManagerFactory: Unable to build Hibernate SessionFactory [persistence unit: default] ; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema validation: missing column [title] in table [products]Flyway không có gì để làm, và Hibernate từ chối khởi động. Đây là bước kiểm tra bắt được một thay đổi entity được deploy mà thiếu migration. Nhưng nó không so sánh đầy đủ. Trước khi có V3, entity đã ghi length = 64 trong khi sku vẫn là VARCHAR(40), và lần khởi động đó vẫn qua được validate:
2026-09-13T17:31:28.504+07:00 INFO 42552 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Schema "public" is up to date. No migration necessary.
2026-09-13T17:31:29.195+07:00 INFO 42552 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'Trong các lần chạy này, validate bắt được column bị thiếu và không so độ dài của varchar. Một column mà entity không map cũng không bị kiểm tra: column description mà V5 thêm sau này không có field nào, và mọi lần khởi động sau đó đều qua.
Đổi tên column bằng V4
Cách sửa cho column bị thiếu là migration trong hình ở phần đầu:
ALTER TABLE products RENAME COLUMN name TO title;2026-09-13T17:32:29.682+07:00 INFO 42696 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 4 migrations (execution time 00:00.012s)
2026-09-13T17:32:29.711+07:00 INFO 42696 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 3
2026-09-13T17:32:29.716+07:00 INFO 42696 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "4 - rename product name to title"
2026-09-13T17:32:29.729+07:00 INFO 42696 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "public", now at version v4 (execution time 00:00.004s)
2026-09-13T17:32:30.430+07:00 INFO 42696 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'docker exec sb-a31-pg psql -U shop -d shop -c 'select id, title, sku from products order by id' -c 'select installed_rank, version, description, script, checksum, success from flyway_schema_history order by installed_rank' id | title | sku
----+---------------------+--------
1 | Mechanical keyboard | KB-01
2 | Wireless mouse | MS-01
3 | USB-C hub | HUB-07
(3 rows)
installed_rank | version | description | script | checksum | success
----------------+---------+------------------------------+--------------------------------------+------------+---------
1 | 1 | create catalog | V1__create_catalog.sql | 18327164 | t
2 | 2 | create orders | V2__create_orders.sql | 678017735 | t
3 | 3 | widen product sku | V3__widen_product_sku.sql | 1537025089 | t
4 | 4 | rename product name to title | V4__rename_product_name_to_title.sql | 715857858 | t
(4 rows)Ba product giữ nguyên tên dưới tên column mới, validate đi qua, và thay đổi có một row của riêng nó.
data.sql vẫn chạy bên cạnh Flyway
Bài 25 nạp dữ liệu mẫu cho catalogue bằng data.sql. Cùng ý tưởng đó cho các table mới:
INSERT INTO categories (name) VALUES ('Keyboards'), ('Mice'), ('Accessories');Trên H2 in-memory, với log DEBUG cho org.springframework.jdbc.datasource.init:
2026-09-13T17:29:55.309+07:00 INFO 42243 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 2 migrations to schema "PUBLIC", now at version v2 (execution time 00:00.011s)
2026-09-13T17:29:55.317+07:00 DEBUG 42243 --- [demo] [ main] o.s.jdbc.datasource.init.ScriptUtils : Executing SQL script from URL [jar:nested:/…/demo/build/libs/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/data.sql]
2026-09-13T17:29:55.318+07:00 DEBUG 42243 --- [demo] [ main] o.s.jdbc.datasource.init.ScriptUtils : 3 returned as update count for SQL: INSERT INTO categories (name) VALUES ('Keyboards'), ('Mice'), ('Accessories')
2026-09-13T17:29:55.319+07:00 DEBUG 42243 --- [demo] [ main] o.s.jdbc.datasource.init.ScriptUtils : Executed SQL script from URL [jar:nested:/…/demo/build/libs/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/data.sql] in 1 ms.
2026-09-13T17:29:55.350+07:00 INFO 42243 --- [demo] [ main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]Flyway không tắt cơ chế SQL initialization. Boot chạy data.sql sau các migration và trước Hibernate, thêm ba row. Trên PostgreSQL thì file bị bỏ qua, vì spring.sql.init.mode là embedded như bài 25 đã đo: lần chạy đầu tiên trên shop có đúng file này trong jar, và các category mà psql thêm sau đó không đụng unique constraint của cột tên. Cùng một project vì thế nạp dữ liệu cho H2 mà không nạp cho production, và không nơi nào ghi lại việc đó.
Hãy xoá data.sql và cho dữ liệu một migration. Dữ liệu mà môi trường nào cũng cần là một versioned migration bình thường trong db/migration. Dữ liệu mẫu cho development đặt ở một location thứ hai:
INSERT INTO categories (name) VALUES ('Keyboards'), ('Mice'), ('Accessories');
INSERT INTO products (name, sku, price, stock, category_id) VALUES
('Mechanical keyboard', 'KB-01', 89.90, 25, (SELECT id FROM categories WHERE name = 'Keyboards')),
('Wireless mouse', 'MS-01', 24.50, 3, (SELECT id FROM categories WHERE name = 'Mice')),
('USB-C hub', 'HUB-07', 39.00, 10, (SELECT id FROM categories WHERE name = 'Accessories'));Chỉ profile development thêm location đó:
spring.flyway.locations=classpath:db/migration,classpath:db/dev-dataspring:
flyway:
locations:
- classpath:db/migration
- classpath:db/dev-dataKhởi động trên H2 với giá trị đó truyền vào qua --spring.flyway.locations, hai thư mục tạo thành một chuỗi version duy nhất:
2026-09-13T17:31:26.330+07:00 INFO 42539 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 3 migrations (execution time 00:00.004s)
...
2026-09-13T17:31:26.350+07:00 INFO 42539 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version "1 - create catalog"
2026-09-13T17:31:26.362+07:00 INFO 42539 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version "2 - create orders"
2026-09-13T17:31:26.366+07:00 INFO 42539 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version "2.1 - sample products"
2026-09-13T17:31:26.370+07:00 INFO 42539 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 3 migrations to schema "PUBLIC", now at version v2.1 (execution time 00:00.011s)Điều gì xảy ra khi một migration thất bại
PostgreSQL rollback toàn bộ migration
V5 thêm một column mô tả và một index để tìm theo title, nhưng câu lệnh thứ hai lại viết theo tên column cũ:
ALTER TABLE products ADD COLUMN description VARCHAR(1000);
CREATE INDEX idx_products_name ON products (name);2026-09-13T17:32:32.004+07:00 INFO 42709 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "5 - add product description"
2026-09-13T17:32:32.016+07:00 ERROR 42709 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migration of schema "public" to version "5 - add product description" failed! Changes successfully rolled back.
...
Caused by: org.flywaydb.core.internal.exception.FlywayMigrateException: Failed to execute script V5__add_product_description.sql
--------------------------------------------------------
SQL State : 42703
Error Code : 0
Message : ERROR: column "name" does not exist
Location : db/migration/V5__add_product_description.sql (/…/demo/nested:/…/demo/build/libs/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/db/migration/V5__add_product_description.sql)
Line : 3
Statement : Run Flyway with -X option to see the actual statement causing the problemCâu lệnh đầu đã thành công trước khi câu thứ hai thất bại. psql sau đó:
docker exec sb-a31-pg psql -U shop -d shop -c '\d products' Table "public.products"
Column | Type | Collation | Nullable | Default
-------------+------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
title | character varying(120) | | not null |
sku | character varying(64) | | not null |
price | numeric(10,2) | | not null |
stock | integer | | not null |
category_id | bigint | | not null |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
"idx_products_category_id" btree (category_id)
"uk_products_sku" UNIQUE CONSTRAINT, btree (sku)
Foreign-key constraints:
"fk_products_category" FOREIGN KEY (category_id) REFERENCES categories(id)
Referenced by:
TABLE "order_lines" CONSTRAINT "fk_order_lines_product" FOREIGN KEY (product_id) REFERENCES products(id)
TABLE "product_tags" CONSTRAINT "fk_product_tags_product" FOREIGN KEY (product_id) REFERENCES products(id)Không có column description, và query trên history table trả về đúng bốn row như sau V4, không có gì cho version 5. PostgreSQL chạy DDL bên trong transaction, Flyway chạy cả file trong một transaction, nên rollback cuốn luôn câu ALTER TABLE; bản thân transaction là chủ đề của bài 30. Vì version 5 chưa từng chạy ở đâu, file của nó vẫn sửa được:
ALTER TABLE products ADD COLUMN description VARCHAR(1000);
CREATE INDEX idx_products_name ON products (name);
CREATE INDEX idx_products_title ON products (title); V5 đã sửa chạy ở lần khởi động sau, log của lần đó nằm trong phần repeatable migration.
MySQL 8.4 giữ lại một nửa
MySQL commit từng câu DDL riêng lẻ. Hai câu lệnh y hệt chạy thành version 2 trên mysql:8.4, sau một V1 viết cho MySQL tạo products với id, title và sku, và build được thêm org.flywaydb:flyway-mysql cùng com.mysql:mysql-connector-j:
2026-09-13T17:37:59.639+07:00 INFO 44431 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema `shop` to version "2 - add product description"
2026-09-13T17:37:59.657+07:00 ERROR 44431 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migration of schema `shop` to version "2 - add product description" failed! Please restore backups and roll back database and code!
...
SQL State : 42000
Error Code : 1072
Message : Key column 'name' doesn't exist in tabledocker exec sb-a31-mysql mysql -t -ushop -psecret shop -e "describe products; select installed_rank, version, description, type, script, checksum, success from flyway_schema_history order by installed_rank;" 2>/dev/null+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| title | varchar(120) | NO | | NULL | |
| sku | varchar(64) | NO | | NULL | |
| description | varchar(1000) | YES | | NULL | |
+-------------+---------------+------+-----+---------+----------------+
+----------------+---------+-------------------------+------+---------------------------------+-------------+---------+
| installed_rank | version | description | type | script | checksum | success |
+----------------+---------+-------------------------+------+---------------------------------+-------------+---------+
| 1 | 1 | create products | SQL | V1__create_products.sql | 1839980680 | 1 |
| 2 | 2 | add product description | SQL | V2__add_product_description.sql | -1426013997 | 0 |
+----------------+---------+-------------------------+------+---------------------------------+-------------+---------+Column của câu lệnh đầu vẫn nằm lại, và version 2 có một row với success bằng 0. Lần khởi động tiếp theo từ chối đi tiếp:
Caused by: org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validation
Detected failed migration to version 2 (add product description).
Please remove any half-completed changes then run repair to fix the schema history.Trên MySQL, mỗi migration một câu DDL giúp một lần thất bại không để lại nửa migration.
Khi nào dùng flyway repair là hợp lý
repair làm hai việc, và log của nó nêu tên cả hai: xoá row của các migration thất bại, và cập nhật checksum đã ghi cho khớp với file. Ở ví dụ MySQL trên, thông báo yêu cầu việc thứ nhất sau khi column bị thêm dở đã được drop bằng tay. Việc thứ hai hợp lý khi một file thay đổi mà không đổi những gì nó làm. Một dòng comment thêm vào V1 lúc review code là thay đổi như vậy:
-- Catalogue tables: categories, tags, products and the product_tags join table
CREATE TABLE categories (Nếu có một bean FlywayMigrationStrategy, Boot gọi nó thay vì tự chạy migrate(). Hai bean như thế, mỗi bean sau một profile riêng, đảm nhận lần repair ở đây và lần thử clean ở phần sau:
package com.example.demo.lab;
import org.springframework.boot.flyway.autoconfigure.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration(proxyBeanMethods = false)
class FlywayLab {
@Bean
@Profile("repair")
FlywayMigrationStrategy repairThenMigrate() {
return flyway -> {
flyway.repair();
flyway.migrate();
};
}
@Bean
@Profile("clean")
FlywayMigrationStrategy cleanThenMigrate() {
return flyway -> {
flyway.clean();
flyway.migrate();
};
}
}java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8131 --spring.profiles.active=postgres,repair --spring.main.web-application-type=none2026-09-13T17:34:44.115+07:00 INFO 43317 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Repair of failed migration in Schema History table "public"."flyway_schema_history" not necessary. No failed migration detected.
2026-09-13T17:34:44.127+07:00 INFO 43317 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Repairing Schema History table for version 1 (Description: create catalog, Type: SQL, Checksum: 115815086) ...
2026-09-13T17:34:44.130+07:00 INFO 43317 --- [demo] [ main] o.f.core.internal.command.DbRepair : Successfully repaired schema history table "public"."flyway_schema_history" (execution time 00:00.024s).
...
2026-09-13T17:34:44.181+07:00 INFO 43317 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 5
2026-09-13T17:34:44.182+07:00 INFO 43317 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Schema "public" is up to date. No migration necessary.docker exec sb-a31-pg psql -U shop -d shop -c "select version, description, checksum from flyway_schema_history where version = '1'" version | description | checksum
---------+----------------+-----------
1 | create catalog | 115815086
(1 row)Chỉ một dòng comment đã đổi checksum của V1 từ 18327164 thành 115815086, nên nếu không có repair nó cũng chặn khởi động giống lần sửa SKU. Khởi động một lần với profile này, rồi bỏ nó đi: nếu để nguyên, strategy sẽ lặng lẽ repair mọi thay đổi ở mọi lần khởi động, kể cả lần sửa SKU. Bên ngoài application, command-line tool của Flyway có lệnh repair tương tự; bài này không dùng nó.
Repeatable migration với prefix R__
File có tên bắt đầu bằng R__ không có version. Flyway chạy nó sau tất cả versioned migration đang chờ và chạy lại mỗi khi checksum của nó đổi, rất hợp với những object được thay nguyên khối, như một view:
CREATE OR REPLACE VIEW product_catalog AS
SELECT p.id, p.sku, p.title, c.name AS category, p.price, p.stock
FROM products p
JOIN categories c ON c.id = p.category_id;File này vào cùng build với V5 đã sửa và chạy ngay sau nó:
2026-09-13T17:32:33.614+07:00 INFO 42742 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "5 - add product description"
2026-09-13T17:32:33.629+07:00 INFO 42742 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" with repeatable migration "product catalog view"
2026-09-13T17:32:33.637+07:00 INFO 42742 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 2 migrations to schema "public", now at version v5 (execution time 00:00.009s)Sau đó view có thêm cờ còn hàng:
CREATE OR REPLACE VIEW product_catalog AS
SELECT p.id, p.sku, p.title, c.name AS category, p.price, p.stock
SELECT p.id, p.sku, p.title, c.name AS category, p.price, p.stock, p.stock > 0 AS in_stock
FROM products p
JOIN categories c ON c.id = p.category_id;2026-09-13T17:32:35.992+07:00 INFO 42755 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 7 migrations (execution time 00:00.014s)
2026-09-13T17:32:36.020+07:00 INFO 42755 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 5
2026-09-13T17:32:36.025+07:00 INFO 42755 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" with repeatable migration "product catalog view"
2026-09-13T17:32:36.038+07:00 INFO 42755 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "public" (execution time 00:00.004s)docker exec sb-a31-pg psql -U shop -d shop -c 'select installed_rank, version, description, type, script, checksum, success from flyway_schema_history order by installed_rank' -c 'select * from product_catalog order by id' installed_rank | version | description | type | script | checksum | success
----------------+---------+------------------------------+------+--------------------------------------+------------+---------
1 | 1 | create catalog | SQL | V1__create_catalog.sql | 18327164 | t
2 | 2 | create orders | SQL | V2__create_orders.sql | 678017735 | t
3 | 3 | widen product sku | SQL | V3__widen_product_sku.sql | 1537025089 | t
4 | 4 | rename product name to title | SQL | V4__rename_product_name_to_title.sql | 715857858 | t
5 | 5 | add product description | SQL | V5__add_product_description.sql | 809655632 | t
6 | | product catalog view | SQL | R__product_catalog_view.sql | -439945004 | t
7 | | product catalog view | SQL | R__product_catalog_view.sql | 1231863337 | t
(7 rows)
id | sku | title | category | price | stock | in_stock
----+--------+---------------------+-------------+-------+-------+----------
1 | KB-01 | Mechanical keyboard | Keyboards | 89.90 | 25 | t
2 | MS-01 | Wireless mouse | Mice | 24.50 | 3 | t
3 | HUB-07 | USB-C hub | Accessories | 39.00 | 10 | t
(3 rows)- Repeatable migration không có version, và mỗi lần chạy thêm một row: 6 và 7 là hai trạng thái của view, với checksum khác nhau.
- File không đổi thì không chạy lại. Lần khởi động sau đó với profile
repair, khi file view không bị động tới, kết thúc bằngSchema "public" is up to date. No migration necessary. - File phải an toàn khi chạy nhiều lần, vì thế nó dùng
CREATE OR REPLACE VIEW.
Thư mục lúc này:
src/main/resources/db
├── dev-data
│ └── V2_1__sample_products.sql
└── migration
├── R__product_catalog_view.sql
├── V1__create_catalog.sql
├── V2__create_orders.sql
├── V3__widen_product_sku.sql
├── V4__rename_product_name_to_title.sql
└── V5__add_product_description.sqlclean-disabled và out-of-order
clean drop mọi object trong các schema mà Flyway quản lý, gồm cả table, view và history table, theo mô tả của tài liệu Flyway về lệnh này. Boot 4.1.1 đặt spring.flyway.clean-disabled=true. Profile clean của FlywayLab gọi flyway.clean() trên shop:
Caused by: org.flywaydb.core.api.FlywayException: Unable to execute clean as it has been disabled with the 'flyway.cleanDisabled' property.Sau đó \dt vẫn liệt kê đủ bảy table và \dv vẫn còn view: lời gọi ném exception trước khi đụng vào bất cứ thứ gì. Hãy giữ nguyên giá trị mặc định cho mọi database có data quan trọng.
spring.flyway.out-of-order liên quan tới một version thấp hơn version đã chạy. Trong database naming, đang ở version 10, một file mới V3__late.sql chặn khởi động:
Caused by: org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validation
Detected resolved migration not applied to database: 3.
To ignore this migration, set -ignoreMigrationPatterns='*:ignored'. To allow executing this migration, set -outOfOrder=true.Với --spring.flyway.out-of-order=true nó chạy, kèm một warning: outOfOrder mode is active. Migration of schema "public" may not be reproducible. Tình huống này xuất hiện khi hai branch cùng thêm migration; đặt cho migration đến sau số tiếp theo còn trống thường gọn hơn là bật thiết lập này.
Đưa một database có sẵn vào Flyway bằng baseline
Database legacy là bản sao của ddlauto tạo trước các lần đổi tên: sáu table do update dựng, ba product, không có history table. Bản build có Flyway khởi động trên nó:
Caused by: org.flywaydb.core.api.FlywayException: Found non-empty schema(s) "public" but no schema history table. Use baseline() or set baselineOnMigrate to true to initialize the schema history table.Flyway không chạy V1 chồng lên các table đã tồn tại. Baseline cho nó biết database đã tương ứng với version nào. legacy có đủ những gì V1 và V2 tạo, nên baseline là version 2:
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=2spring:
flyway:
baseline-on-migrate: true
baseline-version: 2Truyền qua command line cho lần chạy trên legacy:
2026-09-13T17:34:46.935+07:00 INFO 43331 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Schema history table "public"."flyway_schema_history" does not exist yet
2026-09-13T17:34:46.936+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbValidate : Successfully validated 6 migrations (execution time 00:00.009s)
2026-09-13T17:34:46.945+07:00 INFO 43331 --- [demo] [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table "public"."flyway_schema_history" with baseline ...
2026-09-13T17:34:46.959+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbBaseline : Successfully baselined schema with version: 2
2026-09-13T17:34:46.983+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 2
2026-09-13T17:34:46.986+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "3 - widen product sku"
2026-09-13T17:34:46.995+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "4 - rename product name to title"
2026-09-13T17:34:47.001+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "5 - add product description"
2026-09-13T17:34:47.008+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Migrating schema "public" with repeatable migration "product catalog view"
2026-09-13T17:34:47.014+07:00 INFO 43331 --- [demo] [ main] o.f.core.internal.command.DbMigrate : Successfully applied 4 migrations to schema "public", now at version v5 (execution time 00:00.007s)
2026-09-13T17:34:47.683+07:00 INFO 43331 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'docker exec sb-a31-pg psql -U shop -d legacy -c 'select installed_rank, version, description, type, script, checksum, success from flyway_schema_history order by installed_rank' -c '\d products' installed_rank | version | description | type | script | checksum | success
----------------+---------+------------------------------+----------+--------------------------------------+------------+---------
1 | 2 | << Flyway Baseline >> | BASELINE | << Flyway Baseline >> | | t
2 | 3 | widen product sku | SQL | V3__widen_product_sku.sql | 1537025089 | t
3 | 4 | rename product name to title | SQL | V4__rename_product_name_to_title.sql | 715857858 | t
4 | 5 | add product description | SQL | V5__add_product_description.sql | 809655632 | t
5 | | product catalog view | SQL | R__product_catalog_view.sql | 1231863337 | t
(5 rows)
Table "public.products"
Column | Type | Collation | Nullable | Default
-------------+-------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
title | character varying(120) | | not null |
price | numeric(10,2) | | not null |
sku | character varying(64) | | not null |
stock | integer | | not null |
category_id | bigint | | not null |
description | character varying(1000) | | |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
"idx_products_title" btree (title)
"ukfhmd06dsmj6k0n90swsh8ie9g" UNIQUE CONSTRAINT, btree (sku)
Foreign-key constraints:
"fkog2rp4qthbtt2lfyhfo32lsw9" FOREIGN KEY (category_id) REFERENCES categories(id)
Referenced by:
TABLE "product_tags" CONSTRAINT "fk5rk6s19k3risy7q7wqdr41uss" FOREIGN KEY (product_id) REFERENCES products(id)
TABLE "order_lines" CONSTRAINT "fk5v1oeejtgtf2n3toppm3tkuhh" FOREIGN KEY (product_id) REFERENCES products(id)- Row đầu tiên là baseline, type
BASELINE, version 2, không có checksum. V1 và V2 chưa từng chạy trênlegacy; V3, V4, V5 và view thì có. - Phần còn lại của schema vẫn là thứ
updateđã dựng. Unique constraint làukfhmd06dsmj6k0n90swsh8ie9g, foreign key làfkog2rp4qthbtt2lfyhfo32lsw9, vàcategory_idkhông có index. Một database dựng từ V1 lại cóuk_products_skuvàidx_products_category_id, nên một migration sau này gọi tới các tên đó sẽ không tìm thấy gì trênlegacy. Hãy làm hai bên khớp nhau bằng một migration riêng trước khi có migration nào phụ thuộc vào tên constraint. - Bỏ
baseline-on-migratesau lần chạy đầu. Nếu để nguyên, Flyway sẽ baseline bất kỳ schema nào có dữ liệu mà không có history table thay vì từ chối, trong khi chính lời từ chối đó là phần hữu ích của lần thử đầu tiên. Đặtbaseline-versionlà version cao nhất mà database đã có đủ thay đổi; mọi version cao hơn sẽ chạy.
Sinh migration đầu tiên từ entity
Tự viết V1 cho một model đã có sẵn thì chậm, và Hibernate có thể viết bản nháp. Các property schema-generation của Jakarta Persistence sinh ra một script thay vì thay đổi database; với Flyway và ddl-auto đều tắt, application ghi ra script cho các entity hiện tại:
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8131 --spring.profiles.active=postgres --spring.flyway.enabled=false --spring.jpa.hibernate.ddl-auto=none --spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create --spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=build/generated-schema/create.sql --spring.main.web-application-type=nonecreate table categories (id bigint generated by default as identity, name varchar(60) not null unique, primary key (id));
create table order_lines (quantity integer not null, unit_price numeric(10,2) not null, id bigint generated by default as identity, order_id bigint not null, product_id bigint not null, primary key (id));
create table orders (id bigint generated by default as identity, primary key (id));
create table product_tags (product_id bigint not null, tag_id bigint not null, primary key (product_id, tag_id));
create table products (price numeric(10,2) not null, stock integer not null, category_id bigint not null, id bigint generated by default as identity, sku varchar(64) not null unique, title varchar(120) not null, primary key (id));
create table tags (id bigint generated by default as identity, name varchar(40) not null unique, primary key (id));
alter table if exists order_lines add constraint FK1smc0s578t2oih21yn9hw6usr foreign key (order_id) references orders;
alter table if exists order_lines add constraint FK5v1oeejtgtf2n3toppm3tkuhh foreign key (product_id) references products;
alter table if exists product_tags add constraint FKpur2885qb9ae6fiquu77tcv1o foreign key (tag_id) references tags;
alter table if exists product_tags add constraint FK5rk6s19k3risy7q7wqdr41uss foreign key (product_id) references products;
alter table if exists products add constraint FKog2rp4qthbtt2lfyhfo32lsw9 foreign key (category_id) references categories;- Hibernate tự tạo thư mục
build/generated-schema, viết mỗi câu lệnh trên một dòng và kết thúc bằng;.shopkhông bị đụng tới:\dtvẫn liệt kê bảy table và history table vẫn có bảy row. - Script là bản nháp, không phải V1. Foreign key mang tên sinh tự động, unique constraint không có tên, không column foreign key nào có index, và thứ tự column là của Hibernate. Hãy review, đặt tên cho constraint, thêm index, rồi mới lưu nó thành versioned migration đầu tiên.
- Lần chạy thứ hai ghi nối vào file. Khi thêm
--spring.jpa.properties.hibernate.format_sql=true, file không bị thay: một bản đã format của cùng các câu lệnh được nối sau bản đầu, vàcreate.sqltăng lên 78 dòng. Hãy xoá file trước khi sinh lại.
Liquibase ngắn gọn
Liquibase là công cụ migration còn lại mà Spring Boot tự cấu hình. Một bản sao của project thay các dòng Flyway bằng các dòng sau:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-liquibase'
testImplementation 'org.springframework.boot:spring-boot-starter-liquibase-test'
}<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase-test</artifactId>
<scope>test</scope>
</dependency>Jar chứa liquibase-core-5.0.3.jar, snakeyaml-2.6.jar và opencsv-5.12.0.jar, và không cần module riêng cho PostgreSQL. Metadata của Boot đặt giá trị mặc định của spring.liquibase.change-log là classpath:/db/changelog/db.changelog-master.yaml, và Initializr tạo sẵn src/main/resources/db/changelog, để trống. Một changeset:
databaseChangeLog:
- changeSet:
id: create-categories
author: catalog-team
changes:
- createTable:
tableName: categories
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: name
type: VARCHAR(60)
constraints:
nullable: false
unique: true
uniqueConstraintName: uk_categories_nameTrên một database mới, liqui, rút gọn:
2026-09-13T17:38:04.011+07:00 INFO 44447 --- [demo] [ main] liquibase.changelog : Creating database changelog table with name: public.databasechangelog
2026-09-13T17:38:04.072+07:00 INFO 44447 --- [demo] [ main] liquibase.lockservice : Successfully acquired change log lock
2026-09-13T17:38:04.080+07:00 INFO 44447 --- [demo] [ main] liquibase.ui : Running Changeset: db/changelog/db.changelog-master.yaml::create-categories::catalog-team
2026-09-13T17:38:04.086+07:00 INFO 44447 --- [demo] [ main] liquibase.changelog : Table categories created
2026-09-13T17:38:04.087+07:00 INFO 44447 --- [demo] [ main] liquibase.changelog : ChangeSet db/changelog/db.changelog-master.yaml::create-categories::catalog-team ran successfully in 7ms
2026-09-13T17:38:04.092+07:00 INFO 44447 --- [demo] [ main] liquibase.util : UPDATE SUMMARY
2026-09-13T17:38:04.092+07:00 INFO 44447 --- [demo] [ main] liquibase.util : Run: 1
2026-09-13T17:38:04.092+07:00 INFO 44447 --- [demo] [ main] liquibase.util : Previously run: 0
...
2026-09-13T17:38:04.093+07:00 INFO 44447 --- [demo] [ main] liquibase.ui : Liquibase: Update has been successful. Rows affected: 0
2026-09-13T17:38:04.096+07:00 INFO 44447 --- [demo] [ main] liquibase.lockservice : Successfully released change log lock
2026-09-13T17:38:04.790+07:00 INFO 44447 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'docker exec sb-a31-pg psql -U shop -d liqui -c '\dt' -c '\d categories' -c 'select id, author, filename, dateexecuted, orderexecuted, exectype, md5sum, description, liquibase from databasechangelog' List of tables
Schema | Name | Type | Owner
--------+-----------------------+-------+-------
public | categories | table | shop
public | databasechangelog | table | shop
public | databasechangeloglock | table | shop
(3 rows)
Table "public.categories"
Column | Type | Collation | Nullable | Default
--------+-----------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
name | character varying(60) | | not null |
Indexes:
"categories_pkey" PRIMARY KEY, btree (id)
"uk_categories_name" UNIQUE CONSTRAINT, btree (name)
id | author | filename | dateexecuted | orderexecuted | exectype | md5sum | description | liquibase
-------------------+--------------+---------------------------------------+----------------------------+---------------+----------+------------------------------------+----------------------------------+-----------
create-categories | catalog-team | db/changelog/db.changelog-master.yaml | 2026-09-13 17:38:04.087142 | 1 | EXECUTED | 9:46b21e15b2b647823a77b4c3c475729f | createTable tableName=categories | 5.0.3
(1 row)Một changeset được xác định bằng id, author và file changelog chứa nó, cả ba đều nằm trong databasechangelog, còn md5sum là checksum của nó. databasechangeloglock giữ lock mà log cho thấy đã được lấy và nhả ra quanh lần update. Column BIGINT với autoIncrement trở thành generated by default as identity: Liquibase viết DDL cho đúng database nó gặp.
Flyway so với Liquibase
| Flyway 12.4.0 | Liquibase 5.0.3 | |
|---|---|---|
| Starter trong Boot 4.1.1 | spring-boot-starter-flyway | spring-boot-starter-liquibase |
| Hỗ trợ PostgreSQL | module riêng, flyway-database-postgresql | có sẵn trong liquibase-core |
| Location mặc định | classpath:db/migration | classpath:/db/changelog/db.changelog-master.yaml |
| Đơn vị thay đổi | một file, V3__widen_product_sku.sql | một changeset trong changelog, create-categories của catalog-team |
| Thứ tự | version trong tên file | vị trí trong changelog |
| Thứ bạn viết | SQL cho một database | các change type như createTable, được chuyển thành DDL cho database mà nó gặp |
| Lịch sử | flyway_schema_history | databasechangelog và databasechangeloglock |
| Checksum được ghi | 18327164 | 9:46b21e15b2b647823a77b4c3c475729f |
Mỗi project chọn một công cụ. Series này tiếp tục với Flyway: migration của nó là SQL PostgreSQL thuần, và bài tiếp theo sẽ thêm một migration nữa.
Các giá trị ddl-auto so với versioned migration trên database production
| Cách làm | Lúc khởi động, trên database đã có data | Đã thấy trong bài |
|---|---|---|
none | không làm gì; schema phải đến từ nơi khác | mặc định của Boot trên PostgreSQL, và trên H2 khi Flyway được bật |
validate | so mapping với các table và không thay đổi gì | dừng ở missing column [title]; chấp nhận length = 64 so với VARCHAR(40) |
update | thêm table và column, sửa kiểu của column; đổi tên thành một column mới | một column title rỗng, hoặc một lệnh ALTER thất bại chỉ ghi WARN; numeric(10,0) làm tròn 89.90 thành 90 |
create | drop các table được map rồi tạo lại | mỗi lần restart bắt đầu với table rỗng (bài 26) |
create-drop | như create, và drop thêm lần nữa khi tắt | mặc định của Boot trên H2 khi tắt Flyway |
| Versioned migration của Flyway | chạy mỗi file đang chờ đúng một lần, theo thứ tự version, trong transaction riêng trên PostgreSQL, và ghi lại | V4 đổi tên column cùng data; V1 bị sửa chặn khởi động; V5 lỗi được rollback và không có row trong history |
FAQ
Vì sao không nên dùng ddl-auto=update ở production?
Vì nó thay đổi schema đang chạy dựa trên những entity class tình cờ được deploy, không qua review và không để lại dấu vết. Trên PostgreSQL 18.6 với Hibernate 7.4.5, đổi tên một field tạo ra một column rỗng và để data nằm lại ở column cũ, rồi NOT NULL của column cũ chặn mọi câu INSERT; với nullable = false lệnh ALTER thất bại và chỉ có một WARN; còn bỏ scale = 2 làm tròn mọi giá. Database không ghi lại bất cứ điều gì trong số đó.
"Unsupported Database: PostgreSQL 18.6" trong Flyway nghĩa là gì?
Nghĩa là thiếu flyway-database-postgresql. Trong Flyway 12.4.0, flyway-core không chứa phần hỗ trợ PostgreSQL; module kia mới có. Thêm org.flywaydb:flyway-database-postgresql cạnh spring-boot-starter-flyway, như Spring Initializr làm khi chọn PostgreSQL.
Spring Boot tìm Flyway migration ở đâu?
Ở classpath:db/migration, tức src/main/resources/db/migration trong project Gradle hay Maven. Trong Spring Boot 4.1.1, giá trị mặc định được đặt trong constructor của FlywayProperties, không nằm trong property metadata. spring.flyway.locations thay đổi nó và nhận nhiều location, được Flyway gộp thành một chuỗi version.
Sửa lỗi "Migration checksum mismatch" trong Flyway thế nào?
Trả migration về đúng như lúc đã chạy và chuyển thay đổi sang một versioned migration mới, như V3 đã làm với SKU ở đây. Chỉ dùng repair khi thay đổi không đổi những gì migration làm, chẳng hạn thêm một comment, hoặc để xoá row của migration thất bại trên database không có transactional DDL, và chạy nó một lần, ví dụ qua một bean FlywayMigrationStrategy đặt sau một profile.
data.sql có còn chạy khi bật Flyway không?
Có, trên database embedded. Với Spring Boot 4.1.1 và H2, data.sql chạy sau các migration của Flyway và trước khi Hibernate khởi động. Trên PostgreSQL, giá trị mặc định spring.sql.init.mode=embedded bỏ qua nó. Dữ liệu mà môi trường nào cũng cần thì đưa vào một versioned migration, còn dữ liệu cho development thì đặt ở một location thứ hai mà chỉ profile development thêm vào.
Thêm Flyway vào một database có sẵn thế nào?
Đặt spring.flyway.baseline-on-migrate=true và spring.flyway.baseline-version bằng version cuối cùng mà database đã khớp, khởi động một lần, rồi bỏ thiết lập thứ nhất. Không có nó, Flyway 12.4.0 dừng với Found non-empty schema(s) "public" but no schema history table. Hãy chuẩn bị tinh thần rằng tên constraint và index sẽ khác với một database dựng từ các migration.
Kết luận
ddl-auto sinh DDL từ những class tình cờ được deploy. Trên một database PostgreSQL có data, update biến một field bị đổi tên thành một column rỗng hoặc một WARN, bỏ data lại sau một NOT NULL chặn mọi câu INSERT, và làm tròn giá khi scale biến mất, mà không ghi lại gì cả. Flyway biến mỗi thay đổi thành một file SQL đánh số, chạy đúng một lần trên mỗi database, theo thứ tự version, trước khi Hibernate khởi động. flyway_schema_history giữ checksum của từng file, nên một V1 bị sửa chặn khởi động cho tới khi thay đổi được chuyển sang V3; một V5 lỗi được rollback trọn vẹn trên PostgreSQL trong khi MySQL giữ lại một nửa; và validate từ chối một entity có column không đi kèm migration.
Trong Spring Boot 4.1.1, điều đó có nghĩa là spring-boot-starter-flyway cộng với flyway-database-postgresql, mà khi thiếu sẽ hiện ra dưới dạng Unsupported Database: PostgreSQL 18.6, migration nằm trong classpath:db/migration, ddl-auto ở none hoặc validate, và không dùng data.sql, file vẫn chạy cạnh Flyway trên H2. Một database có sẵn được đưa vào bằng một lần baseline, view thuộc về repeatable migration, clean luôn bị tắt, và script do Hibernate sinh ra là bản nháp cần review trước khi trở thành migration.
Bài tiếp theo xây trên các table này với auditing: @CreatedDate, @LastModifiedDate và @CreatedBy ghi lại ai tạo, ai sửa một row và vào lúc nào, cùng một migration mới để thêm các column của chúng.