Spring Boot 4.0 整合 Flyway:数据库版本管理
前言
如果建表 SQL 只存在于某个人的终端历史里,团队很快就无法回答:测试库当前到底是什么结构?某个索引是什么时候加的?生产升级失败后执行到了哪一步?
Flyway 把每次结构变化保存为不可变版本脚本,并在数据库中记录执行结果和校验和。
本章会检查订单项目的三次迁移:核心表、PostgreSQL 高级索引、租户与分片表。
配套源码:dvsusan/spring-boot4-demo。
最终结果
启动 db Profile 后,数据库自动出现:
flyway_schema_history
customers
purchase_orders
order_items
inventory
audit_logs
shard_order_0
shard_order_1
tenant_acme.tenant_notes
tenant_globex.tenant_notes查询迁移历史:
docker compose exec postgres \
psql -U spring_boot4 -d spring_boot4 \
-c "SELECT installed_rank, version, description, success FROM flyway_schema_history ORDER BY installed_rank;"
图 1:每个版本只向前执行一次,校验和阻止已发布脚本被偷偷改写。
第 1 步:添加 Flyway 依赖
<dependency>
<!-- 提供迁移引擎和 Spring Boot 启动集成。 -->
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<!-- PostgreSQL 支持从 Flyway 10 起拆为独立模块。 -->
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>只有 flyway-core 而没有 PostgreSQL 模块时,应用可能提示缺少数据库支持。版本由 Spring Boot 4 的依赖管理统一控制。
第 2 步:按版本命名迁移
目录:
demo-persistence/src/main/resources/db/migration/
├── V1__create_order_core_tables.sql
├── V2__add_postgresql_metadata_and_indexes.sql
└── V3__add_tenant_schemas_and_shards.sql版本脚本必须是:
V开头。- 全项目唯一且递增。
- 双下划线分隔版本和描述。
- 已执行后不再修改。
文件名会进入 flyway_schema_history。修改已执行脚本内容会改变校验和,随后启动默认失败。
第 3 步:V1 建立核心结构
V1 只创建客户、订单、订单项、库存和审计:
-- 核心表不包含 PostgreSQL 专有索引,先保证基本关系成立。
CREATE TABLE purchase_orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
tenant_id VARCHAR(32) NOT NULL,
customer_id BIGINT NOT NULL
REFERENCES customers(id),
amount NUMERIC(12, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_order_tenant_no UNIQUE (tenant_id, order_no)
);
CREATE TABLE order_items (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL
REFERENCES purchase_orders(id),
product_id VARCHAR(32) NOT NULL,
product_name VARCHAR(120) NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(12, 2) NOT NULL
);迁移脚本可以有数据库约束,但不要把大段测试数据、管理员密码或环境专属 ID 写进正式迁移。
第 4 步:V2 增加专有能力
V2 增加 JSONB 和全文检索:
-- 渠道、活动等弹性字段进入 JSONB。
ALTER TABLE purchase_orders
ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}'::jsonb;
-- 商品名生成全文向量,不需要应用层手工维护。
ALTER TABLE order_items
ADD COLUMN search_vector TSVECTOR
GENERATED ALWAYS AS (
to_tsvector('simple', product_name)
) STORED;
CREATE INDEX idx_order_metadata_gin
ON purchase_orders USING GIN (metadata jsonb_path_ops);
CREATE INDEX idx_order_item_search_gin
ON order_items USING GIN (search_vector);把高级索引单独放入 V2,可以清楚看到它是从基础结构演进而来,也方便不同环境逐步上线。
第 5 步:V3 增加 Schema 与分片表
-- 每个租户使用独立 Schema。
CREATE SCHEMA IF NOT EXISTS tenant_acme;
CREATE SCHEMA IF NOT EXISTS tenant_globex;
CREATE TABLE tenant_acme.tenant_notes (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
note_key VARCHAR(64) NOT NULL UNIQUE,
note_value VARCHAR(200) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);分片表也使用同样迁移:
-- 应用层按订单号哈希选择其中一张表。
CREATE TABLE shard_order_0 (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE,
tenant_id VARCHAR(32) NOT NULL,
customer_no VARCHAR(32) NOT NULL,
amount NUMERIC(12, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE shard_order_1 (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE,
tenant_id VARCHAR(32) NOT NULL,
customer_no VARCHAR(32) NOT NULL,
amount NUMERIC(12, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);真实扩容时不要只复制建表语句,还要设计数据迁移、双写、校验和切流。
第 6 步:配置 Flyway 与 JPA 的职责
application-db.yml:
spring:
jpa:
hibernate:
# Hibernate 只检查实体与迁移结果是否一致。
ddl-auto: validate
flyway:
enabled: true
locations: classpath:db/migration
validate-on-migrate: true生产环境禁止 ddl-auto=update 自动改表。自动更新不可审计,也无法可靠处理数据回填、索引创建和大表锁。
Flyway 先执行迁移,Hibernate 随后校验 Entity 映射。若实体多一个不存在字段,应用启动失败,而不是运行到某个接口才报错。
第 7 步:启动并观察迁移
docker compose up -d
mvn clean package -DskipTests
java -jar demo-web/target/demo-web-1.0.0-SNAPSHOT.jar \
--spring.profiles.active=db日志应包含迁移摘要,关键内容是 Successfully applied 3 migrations。
再次启动应用。Flyway 检测到历史已是最新版本,不会重复执行 DDL。
查询:
docker compose exec postgres \
psql -U spring_boot4 -d spring_boot4 \
-c "SELECT version, description, checksum, success FROM flyway_schema_history ORDER BY installed_rank;"预期三行版本 1、2、3,success 都为 t。
第 8 步:验证关键索引
docker compose exec postgres \
psql -U spring_boot4 -d spring_boot4 \
-c "SELECT indexname FROM pg_indexes WHERE indexname LIKE 'idx_%' ORDER BY indexname;"应看到订单客户索引、JSONB GIN 索引和全文 GIN 索引。索引存在不等于查询一定使用它,还要用 EXPLAIN ANALYZE 检查选择性。
第 9 步:生产迁移规则
推荐“扩展—迁移—收缩”:
- 先增加新列或新表,兼容旧代码。
- 部署同时读写新旧结构的应用。
- 后台回填并校验数据。
- 切换读取到新结构。
- 确认旧客户端退出后再删除旧列。
大表加索引前评估锁时间,必要时使用数据库支持的并发建索引方案。迁移脚本执行前必须备份或确认恢复点。
常见问题排查
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 迁移脚本没执行 | Profile 未激活或位置错误 | 检查 db Profile 和 classpath |
| 校验和不一致 | 修改了已执行脚本 | 新增后续版本修复,不要改历史 |
| 缺少 PostgreSQL 支持 | 只引入 flyway-core | 增加 flyway-database-postgresql |
| 启动时 JPA 校验失败 | 实体与数据库列不一致 | 对比映射和迁移结果 |
| 迁移中途失败 | DDL、约束或数据不兼容 | 还原环境,修复后新增版本 |
| 多实例同时启动 | 两边都尝试迁移 | 使用部署流水线单点迁移或数据库锁 |
本章小结
Flyway 把结构变化变成可审计、可重复的发布物:
- 已执行脚本不可改。
- JPA 只校验映射。
- 结构升级必须考虑兼容窗口。
- 大表迁移要有锁、回填和回滚方案。
下一章会使用两个独立 Schema 和动态 DataSource 展示多租户隔离。
练习题
设计 V4 为订单增加 paid_at:
- 先新增可空列。
- 部署代码写入支付时间。
- 回填历史已支付订单。
- 加非空约束前先确认无遗漏。
- 写出失败后的数据恢复步骤。
相关阅读:
13 个企业级实战项目 · 源码 + 教程 + 答疑
简历包装指导AI Agent 实战高并发·微服务Offer 导向持续更新3000+ 同行交流1V1 答疑3 天包退
覆盖高并发、微服务、分布式与 AI Agent:100万QPS短链、企业智能知识库、商城微服务、SaaS点餐、秒杀、刷题小程序、代码生成器、AI Agent 等。券后 ¥129(原价 ¥159),不满意 3 天包退。
