简介:这是一份面向Java开发者的DropwizardDB与Flyway数据库迁移集成指南,适合正在使用或计划使用Dropwizard框架构建服务、并希望以Flyway自动化管理数据库版本的技术人员。文档从背景与动机、目标受众切入,依次介绍DropwizardDB与Flyway的核心特性、开发环境与数据库准备工作、项目创建及依赖配置,再深入讲解Flyway的基础配置参数、迁移脚本命名规则、基本SQL与高级脚本编写技巧。后半部分则给出在DropwizardDB中集成Flyway的完整步骤,包含单元测试与集成测试设计、迁移执行与数据库结构验证、性能与异常处理测试,并整理常见问题、最佳实践、回滚策略和未来扩展方向。资源为单个PDF文件,大小约4.62MB,内文目录完整,支持章节跳转和左侧大纲定位,便于快速查阅。目前已有63人学习下载,适合有一定Java基础、正在工程中落地数据库迁移方案的开发者参考。
1. DropwizardDB 遇上 Flyway:数据库结构变更不能再靠手工
接入新数据库结构最怕的不是写 SQL,而是不知道线上库已经跑到哪个版本。早年在 Dropwizard 项目里我手动执行过一条 ALTER TABLE,结果第二天同事在测试环境重放初始化脚本,直接把刚加的字段覆盖了。DropwizardDB 里的 DataSourceFactory 只负责把连接池、JDBC URL、账号密码配好并交给 JDBI 或 Hibernate 用,它不管理结构变更。结构变更管不好,就得靠 Flyway 这类带版本号的数据库迁移工具兜底。Flyway 的思路很直接:每次结构变更写成一份带版本号的 SQL 文件,按顺序执行并记录到 schema_history 表,下次运行就知道哪些执行过了。这篇笔记从依赖配置、迁移脚本编写、启动流程绑定写到线上踩坑,把 Dropwizard 集成 Flyway 数据库迁移的完整方案讲清楚,适合正在做 Dropwizard 服务端开发、被数据库结构同步折磨过的同学参考。
2. 在 Dropwizard 工程里接 Flyway:依赖、配置和目录三件事
2.1 Maven 依赖选型:dropwizard-db 与 flyway-core 的版本搭配
Dropwizard 项目接 Flyway 的第一步,是在 pom.xml 里补齐依赖。Dropwizard 通过 dropwizard-db 模块提供 DataSource 的配置和连接池管理,但结构迁移它完全不管,你需要单独引入 Flyway。常见做法是 flyway-core 加一个数据库专属模块,比如用 PostgreSQL 就再加 flyway-database-postgresql,用 MySQL 则换成 flyway-mysql。
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-db</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>${flyway.version}</version> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-database-postgresql</artifactId> <version>${flyway.version}</version> </dependency>逻辑说明:Flyway 9.x 开始按数据库类型拆分了模块,PostgreSQL 必须显式引入 flyway-database-postgresql,否则运行时会直接抛 Unsupported Database: PostgreSQL。核心的 flyway-core 负责迁移引擎和版本管理,数据库模块负责方言识别和类型映射——只引 core 不引数据库模块,项目能编译过,一跑就翻车。
参数说明:版本号有三个点要盯住。第一,dropwizard-db 跟随 Dropwizard 主版本,比如 Dropwizard 2.1.x 就配 dropwizard-db 2.1.x;第二,flyway-core 和数据库模块必须用同一个版本号,混着配大概率遇到 NoSuchMethodError 这类诡异报错;第三,Flyway 10.x 要求 JDK 17,Flyway 9.x 支持 JDK 11,Flyway 8.x 才兼容 JDK 8。如果你的服务还跑在 JDK 8 上就用 Flyway 8.x——迁移工具嘛,老版本稳定胜过一切。
除了自己拼依赖,也有人用社区维护的 dropwizard-flyway bundle,把 Flyway 封装成 Dropwizard Bundle,在 Bootstrap.initialize() 里 addBundle 一行注册。这类封装的好处是配置更贴近 Dropwizard 风格,但维护节奏通常追不上 Dropwizard 主版本的更新速度。我一般不推荐,直接在 Application 类里调 Flyway 官方 API 更透明,少一层封装就少一类版本兼容问题。
2.2 config.yml 里的 Flyway 配置项:从 locations 到 baselineOnMigrate
依赖就绪后是配置。Dropwizard 的 database 配置块由 DataSourceFactory 解析,Flyway 的配置项需要在 config.yml 里单独开一个命名空间,并配一个对应的配置类接收。这样 dev、staging、prod 各环境只改 YAML,不动代码。
database: driverClass: org.postgresql.Driver user: app_user password: ${DB_PASSWORD} url: jdbc:postgresql://localhost:5432/myapp flyway: locations: - classpath:db/migration baselineOnMigrate: true validateOnMigrate: true connectRetries: 3 connectRetriesInterval: 5逻辑说明:database 块是 dropwizard-db 的标准配置,flyway 块是自定义的。Flyway 从 DataSource 里读取连接信息,所以 flyway 块里不需要重复写 URL 和账号。locations 指定迁移文件的扫描路径,classpath:db/migration 对应 src/main/resources/db/migration 目录。connectRetries 和 connectRetriesInterval 是给数据库还没完全就绪时的重试机会——容器化部署里这个配置很实用,DB 容器可能比应用容器晚启动几秒,没有重试的话应用直接启动失败。
参数说明:baselineOnMigrate 是连接存量数据库的关键开关。线上库如果已经有业务表但从来没跑过 Flyway,首次 migrate 会因为 schema 非空而报错——Flyway 不知道这些表是谁建的。打开 baselineOnMigrate 后,它会在 schema_history 表里写入一条基线记录(默认版本 1),把「这个库里已存在表」标记下来,后续迁移脚本从版本 2 开始执行。validateOnMigrate 则是每次 migrate 前先做校验,对比已执行迁移文件的 checksum 与表里的存值,不一致直接中断,防止有人悄悄改历史脚本。
public class FlywayConfiguration { private List<String> locations = List.of("classpath:db/migration"); private boolean baselineOnMigrate = true; private boolean validateOnMigrate = true; private int connectRetries = 3; private int connectRetriesInterval = 5; // getter / setter 省略 }这个配置类本身不复杂,但字段名要和 YAML 里的键严格对应,Dropwizard 的 Jackson 反序列化才能把值正确填进去。配置类最好放在和 Application 类同一个包里,用 @JsonIgnoreProperties(ignoreUnknown = true) 避免新增字段时老的 YAML 解析报错。
2.3 迁移文件目录约定:db/migration 下怎么组织 SQL
Flyway 默认扫描 classpath:db/migration。注意事项是 Flyway 不递归扫描子目录,如果按业务模块分了子文件夹,必须把每个子目录都显式配成一个 location。比如配置 locations 为 classpath:db/migration/order 和 classpath:db/migration/user,Flyway 才会在两处分别找迁移文件。
另一种容易踩坑的情况是目录存在但文件没放进去。Flyway 扫描 locations 时,如果目录不存在默认不报错,只会记录一条警告。我排查过一个案例:同事把 V3 脚本放到了 src/main/java 下面而不是 resources 里,结果 classpath 上根本找不到这个文件,migrate 输出 Successfully applied 0 migrations,但代码里已经引用了 V3 创建的新字段,应用启动就 SQL 报错。这个配置了但没生效的假象特别难查,建议每个新脚本落地后都看一眼 schema_history 表,确认迁移确实执行了。
关于目录层级,我一般这样组织:
| 场景 | 目录结构 | 说明 |
|---|---|---|
| 单体应用 | src/main/resources/db/migration/V1__xxx.sql | 直接平铺,简单直接 |
| 多模块项目 | 每个模块独立配置 locations 指向自己的 db/migration | 各模块各自管理 schema 变更 |
| 多数据库服务 | db/migration/user 与 db/migration/order 分开 | 每个库独立的迁移时间线 |
这个表的最后一行很重要:如果两个业务库共用一个迁移目录,发布时一个库执行了 V3 而另一个库还没执行,下一次两个库再同时跑 migrate 时,版本序列会乱掉。多数据库服务里迁移目录隔离是必须的,不只是组织习惯,更是运维边界。
3. 写第一组迁移脚本:从建表到加索引的完整落地
3.1 迁移文件命名规范:版本号、双下划线与描述语法
Flyway 的迁移文件命名严格遵循 V{版本号}__{描述}.sql 的格式。版本号之后是双下划线,描述部分只能由字母、数字、下划线组成,它会被写进 schema_history 表供人阅读。版本号支持点分,比如 V1.10 与 V1_10 等价,Flyway 在比较版本时会按点号拆开逐级比较——V1.10 大于 V1.9,这不是字符串比较,容易忽略但很重要。
版本号一旦在某环境执行过,就永远不能改。我见过有人为了绕过 out-of-order 校验,把版本号从 V3 改成 V10,但 schema_history 表里存的版本、描述和 checksum 还是原来那一套,改完文件名下次 validate 直接报 checksum mismatch。改版本号不是解决办法,唯一正确的路是新增一个更高版本的迁移来覆盖变更。
还有一类 R 前缀的迁移叫 Repeatable,格式是 R__{描述}.sql,没有版本号。这类迁移在每次执行 migrate 时只要内容有变化就会重新执行,适合放视图、存储过程这类需要幂等覆盖的对象。社区版没有 undo 能力,U 前缀的脚本在社区版不会被执行,不要指望它。
3.2 V1__create_users_table.sql:建表迁移怎么写得稳
第一个迁移脚本最简单,但往往决定了后续所有脚本的基调。
-- V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, display_name VARCHAR(100), status VARCHAR(20) NOT NULL DEFAULT 'active', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); CREATE INDEX idx_users_status ON users (status);逻辑说明:这份脚本包括了用户表和一个 status 索引。BIGSERIAL 和 TIMESTAMP WITH TIME ZONE 是 PostgreSQL 专属语法,如果目标是 MySQL,得换成 BIGINT AUTO_INCREMENT 和 DATETIME。这说明迁移脚本天然是数据库相关的,Flyway 不会转换方言,一套脚本在 PG 上跑通不代表能在 MySQL 上复用。
参数说明:我给 status 建了索引,但故意没有给 email 建唯一约束,留给 V2 迁移单独处理。这么分是因为唯一约束的创建可能因为历史数据重复而失败,而 status 索引的创建几乎不会受已有数据影响。在迁移脚本里区分「一定会成功的操作」和「可能因为数据出问题的操作」,能减少很多不必要的失败。另一个细节是建表脚本里的 NOT NULL DEFAULT NOW()——created_at 和 updated_at 都给默认值,这样以后写数据代码时可以少关心一列,但要注意迁移之后应用层的 INSERT 语句不带这些列时,默认值会兜底,行为比没有默认值更可控。
建表脚本还有一个容易忽略的点:如果 V1 有多个语句,建议全部包在一个事务里执行(PostgreSQL 天然支持 DDL 事务),任何一条语句失败都会回滚整个 V1,不会留下半张表。MySQL 做不到这一点,所以 MySQL 项目里 V1 脚本最好把 CREATE TABLE 拆成最小单元,避免一个失败留下一个残缺表。
3.3 V2__add_email_unique_index.sql:增量变更的正确写法
第二个迁移脚本要覆盖的是最常见的增量变更:给已有表加唯一约束。这个操作在数据不干净时是必然失败的。
-- V2__add_email_unique_index.sql -- 先清理重复 email,再建唯一索引 DELETE FROM users WHERE id NOT IN ( SELECT MIN(id) FROM users GROUP BY email ); ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);逻辑说明:这个 DELETE 语句不是可选的,而是前置条件。建唯一索引要求列值全局唯一,如果 users 表里已经存在重复 email,ALTER TABLE 会直接失败,Flyway 会把这次迁移标记为 failed,后续所有迁移都被卡住。把清理逻辑写进迁移脚本,是保证迁移在真实环境下能成功执行的标准做法。
参数说明:这个 DELETE 在 PostgreSQL 上可行,但要注意大表性能。users 表在几十万行时,这个 NOT IN 子查询可能跑几分钟,并锁住相关行。更稳的方式是分两步:先查重复项的数量和分布,确认可接受再用 LIMIT 分批删。迁移脚本每个版本只执行一次,性能不是首要考量,但避免把数据库长时间锁住导致线上不可用,同样重要。
这里也涉及一个事务性细节:PostgreSQL 的 DDL 在事务里可以回滚,DELETE 和 ALTER TABLE 要么一起成功、要么一起回滚。MySQL 则不同,ALTER TABLE 执行到一半失败后无法回滚,表结构可能处于中间状态,Flyway 会记录 failed,需要手动补齐后续变更再用 repair 清记录。这也是我在生产环境优先选 PostgreSQL 的原因之一,DDL 能进事务真的很省心。
3.4 执行与验证:migrate 命令和 schema_history 表怎么看
迁移脚本写好后,开发环境一般用 Maven 插件执行。
mvn flyway:migrate \ -Dflyway.url=jdbc:postgresql://localhost:5432/myapp \ -Dflyway.user=app_user \ -Dflyway.password=secret执行完成后一定要看一眼 schema_history 表确认结果。
SELECT installed_rank, version, description, success FROM flyway_schema_history ORDER BY installed_rank;如果返回两行,版本分别是 1 和 2,success 都是 true,说明迁移已正确执行。如果出现 success=false 的记录,说明这次迁移失败了——先排查原因,不要立刻重跑 migrate,Flyway 不会自动重试失败的迁移,它会持续报错,直到你手动修复或者 repair。
参数说明:installed_rank 是安装顺序号,version 和 description 对应脚本文件名,success 表示执行结果。日常排错我最常看的是 version 和 success 两列。如果 version 连续但 description 不符合预期,说明有人改过脚本文件名;如果 installed_rank 的顺序和版本号排序不一致,大概率是开了 outOfOrder 选项。
4. 把迁移挂进 Dropwizard 启动流程:run() 方法里的执行顺序
4.1 在 run() 里创建 Flyway 实例:裸调 API 是最直接的方案
Application 类的 run() 方法是 Dropwizard 服务启动的必经之路。把 Flyway 的 migrate 调用直接写进 run(),是集成方式里最简单、也最可预测的一种。
public class OrderServiceApplication extends Application<OrderServiceConfiguration> { @Override public void run(OrderServiceConfiguration config, Environment env) { DataSourceFactory dataSourceFactory = config.getDataSourceFactory(); DataSource dataSource = dataSourceFactory.build(env.metrics(), "order-db"); Flyway flyway = Flyway.configure() .dataSource(dataSource) .locations("classpath:db/migration") .baselineOnMigrate(config.getFlyway().isBaselineOnMigrate()) .validateOnMigrate(config.getFlyway().isValidateOnMigrate()) .load(); flyway.migrate(); final JDBI jdbi = new JDBI(dataSource); env.jersey().register(new UserResource(jdbi)); } }逻辑说明:先通过 DataSourceFactory.build() 拿到 Dropwizard 管理的数据源,再把它传给 Flyway.configure()。dataSource() 参数类型是 javax.sql.DataSource,Dropwizard 的 DataSourceFactory 返回的 HikariCP 连接池实现了这个接口,可以直接传。调用 migrate() 之后再初始化 JDBI 或 Hibernate,这样业务层拿到的数据源背后已经是迁移完成后的最新结构。
参数说明:这里有两个细节值得聊。第一,Flyway.configure().load() 返回的是可复用的 Flyway 实例,如果后续还需要调 validate() 或 info() 可以继续用同一个实例;只执行一次迁移的话也可以直接连调 .migrate(),Flyway 会自动完成加载。第二,baselineOnMigrate 和 validateOnMigrate 读的是配置对象里的值,这样生产环境和本地环境可以通过 YAML 差异化配置行为。生产环境几乎总是保持 validateOnMigrate=true,它是把住数据一致性最后一道防线。
4.2 用 Managed 接口保证「先迁移、后对外服务」
直接把 migrate() 放在 run() 里,对大多数系统是没问题的——数据库没就绪,服务就不该启动。但如果你想对 Flyway 的启动阶段和 JDBI、Hibernate 的初始化做更明确的顺序控制,实现 Managed 接口是更工程化的做法。
public class FlywayMigrationManager implements Managed { private final Flyway flyway; public FlywayMigrationManager(Flyway flyway) { this.flyway = flyway; } @Override public void start() { flyway.migrate(); } @Override public void stop() { // Flyway 不持有长期连接,无需清理 } }在 Application.run() 里注册:
env.lifecycle().manage(new FlywayMigrationManager(flyway));逻辑说明:Dropwizard 的 Environment.lifecycle().manage() 接收 Managed 接口实现,start() 会在服务对外监听端口之前依次执行,stop() 在服务关闭时执行。这样 Flyway 的迁移成为生命周期里的一个环节,而不是 run() 里散落的逻辑。start() 中的 migrate() 如果抛出异常,Dropwizard 会中断整个服务启动,这是期望行为——没有适配最新数据库结构的服务不应对外提供请求。
参数说明:一个容易忽略的细节是,LifecycleManager 会先执行所有 Managed 组件的 start(),再启动 Jetty。如果你有多个 Managed 组件,比如 FlywayMigrationManager 后面还有一个缓存预热组件,不要假定它们的 start() 执行顺序——它们都在 Jetty 启动前全部执行完,互相之间不感知对方的数据变更。如果确实有上下游依赖,把两者合成一个 Managed,在 start() 里按序调用。
4.3 多数据源场景下每个数据源独立迁移
一个 Dropwizard 服务连多个数据库很常见,比如订单库和用户库分开。Flyway 的方案是为每个数据源创建独立的实例、独立的迁移目录。
Flyway userDbFlyway = Flyway.configure() .dataSource(userDataSource) .locations("classpath:db/migration/user") .load(); userDbFlyway.migrate(); Flyway orderDbFlyway = Flyway.configure() .dataSource(orderDataSource) .locations("classpath:db/migration/order") .load(); orderDbFlyway.migrate();逻辑说明:每个 Flyway 实例在各目的 schema 下维护独立的 schema_history 表,迁移互不干扰。这样业务库可以单独演进、单独迁移,而不必在同一个迁移目录里处理不同库的变更,那会让版本序列变得极其混乱。
参数说明:如果两个数据源落在同一个 PostgreSQL 实例上,可以用 schemas() 参数指定 schema 归属,再指定对应的 locations。最常见的配置错误是:locations 指向同一个目录,但 schemas 不同,结果两个 Flyway 实例对同一个 schema 重复执行迁移。要确保每个 Flyway 实例的 locations 和 schemas 一一对应。
另一个细节是执行顺序:如果订单表的迁移脚本里引用了用户表的某些数据(比如外键),那 userDbFlyway.migrate() 必须先于 orderDbFlyway.migrate() 执行。这个依赖关系无法通过配置表达,只能在代码顺序上保证,建议在代码注释里写清楚。
4.4 测试环境如何触发迁移:dropwizard-testing 与 Flyway 配合
集成测试需要一个结构完整的测试数据库。推荐的做法是在测试基类里用 Flyway 直接迁移测试库,再用 dropwizard-testing 的 DropwizardAppRule 启动应用。
@ClassRule public static final DropwizardAppRule<TestConfiguration> RULE = new DropwizardAppRule<>(TestApplication.class, "test-config.yml"); @BeforeClass public static void migrateTestDb() { DataSourceFactory factory = RULE.getConfiguration().getDataSourceFactory(); DataSource dataSource = factory.build(new MetricRegistry(), "test-db"); Flyway.configure() .dataSource(dataSource) .locations("classpath:db/migration") .load() .migrate(); }逻辑说明:这个模式的要点是用 RULE 拿到应用配置里的 DataSourceFactory,构建连接测试库的 DataSource,然后用 Flyway 对测试库执行迁移。测试用例本身通过 HTTP 请求访问应用,数据库里已经有迁移后的表结构。测试库需要在每次跑测试前重置,CI 里最稳的方式是 Testcontainers 起全新 PostgreSQL 容器,迁移脚本从头执行,干净利落。
参数说明:测试环境一个常见痛点是迁移脚本里包含大量历史数据清理逻辑,每次测试全量跑一遍耗时很长。如果只是验证某些业务逻辑,可以先 Flyway.clean() 清理库结构再 migrate() 重建,也可以只用 target() 参数指定迁移到某个版本。但要注意 target() 在生产环境不应使用,否则会跳过后续迁移,让 schema 处于不完整状态。
5. Flyway 集成避坑指南:5 个从线上带回来的故障现场
5.1 checksum mismatch:改了历史迁移文件,数据库版本对不上
现象:一次上线执行 migrate 时控制台直接报 FlywayException: Validate failed: Migration checksum mismatch for migration version 2。当时的第一个反应是查 git 历史,果然有人为了修一个索引名在 V2 脚本里改了一行。
原因:Flyway 在每个迁移文件执行成功后会计算文件内容的 checksum 并写入 schema_history 表。validateOnMigrate 开启时,每次 migrate 前 Flyway 会重新计算所有已执行迁移文件的 checksum,和表里存的比对,不一致就拒绝继续。这是防止历史迁移被篡改的后手,是保护机制,不是误报。
解决:不能修改已经执行过的迁移文件。对历史迁移的一切更正,都必须通过新增一个更高版本的迁移来实现。如果这个脚本确实只在本地开发跑过、从没推过共享环境,可以先用 flyway repair 清除旧 checksum 再重新执行——但判断标准要看 schema_history 表里有没有记录,只要有记录,就说明已经生效了,改它就是在制造 mismatch。
5.2 out-of-order 报错:版本号回退引发的执行顺序冲突
现象:两个开发分支各写了 V3 和 V4 迁移,V4 先合入并部署了,V3 后合入——merge 后代码里同时存在 V3 和 V4,但数据库已经执行过 V4,Flyway 拒绝执行 V3。
原因:Flyway 默认只允许执行版本号大于当前已执行最大版本的迁移。V3 小于 V4,被判定为 out-of-order。它不允许乱序执行,是因为 V3 的操作可能在 V4 的变更之后才应用,万一 V3 里修改了 V4 依赖的列,就会出兼容性问题。这个限制保护的是你,不是为难你。
解决:把 V3 重命名成 V5,让版本号大于已执行的最大版本。这个操作在代码 review 阶段就应该做——合并时检查迁移目录里是否有版本号回退文件。不要轻易开启 outOfOrder=true,它会打乱 schema_history 的 installed_rank 顺序,后续排查问题时很难说清楚某个表结构是哪个版本建出来的。
5.3 多 schema 租户场景:locations 与 schemas 配置没对上
现象:多租户系统每个租户一个 schema,迁移脚本在租户 A 的 schema 下执行成功,但租户 B 的 schema 里没有任何表。
原因:Flyway 默认只迁移 DataSource 连接默认的 schema。如果不显式指定 schemas(),它只会在默认 schema 下创建 schema_history 表并执行迁移脚本。多租户场景需要让 Flyway 遍历每个 schema 单独迁移。
解决:循环遍历租户 schema 列表,为每个 schema 创建 Flyway 实例,分别调用 migrate()。或者用 Flyway 的 schemas() 参数一次性传入多个 schema 名,它会逐一执行迁移。但要注意迁移脚本里不能写死 schema 名——SQL 里一旦出现 tenant_a.users 这种全限定名称,无论在哪个 schema 下执行,最终打的都是同一张表。
5.4 HikariCP 连接池代理后的 DataSource 导致迁移失败
现象:项目里先初始化了 HikariCP 连接池,再把 pooled DataSource 传给 Flyway.dataSource(),迁移时报 SQLException: Connection is closed,而且时好时坏。
原因:Flyway 在迁移过程中频繁获取和释放连接,当 DataSource 本身经过多层代理包装时,某些数据库驱动的元数据接口拿到的连接状态不稳定。另一个常见情况是 HikariCP 的 leakDetectionThreshold 把 Flyway 迁移过程中的长事务误判为连接泄漏,强制回收连接,导致迁移中断。
解决:直接使用 DataSourceFactory.build() 返回的对象传给 Flyway,不要在外面再包一层连接池。Dropwizard 的 DataSourceFactory 返回的就是连接池对象,Flyway 通过它获取和释放连接,迁移结束后归还池中。如果仍然遇到连接关闭报错,检查迁移脚本是否在单事务里执行了过多操作,PostgreSQL 上大脚本执行超过 HikariCP 的 connectionTimeout(默认 30 秒)就可能被强制回收,需要调大超时或把大迁移拆成多个版本。
5.5 repair 和 baseline 的正确使用场景
现象:迁移失败后,schema_history 表里追加了一条 success=false 记录。有人觉得删掉这条记录就好了,结果下一次 migrate 时 Flyway 重新执行同一个脚本,报重复建表。
原因:flyway_schema_history 表是 Flyway 判断迁移是否执行的唯一依据。手动 DELETE 掉失败记录,Flyway 会认为该迁移从未执行过,重跑时执行同样的 DDL 当然会撞上已存在的表。正确流程是:先查明失败原因,把数据库手动恢复到该迁移应该达成的状态(要么完成剩余操作,要么回滚已执行部分),然后执行 flyway repair 清除失败记录,再重新 migrate。
mvn flyway:repair \ -Dflyway.url=jdbc:postgresql://localhost:5432/myapp \ -Dflyway.user=app_user \ -Dflyway.password=secret逻辑说明:flyway repair 不是回滚工具,它的职责是让 schema_history 表恢复一致——清除失败记录、重新计算已执行脚本的 checksum、移除冗余的 baseline 标记。Flyway 社区版没有真正的 undo 能力,出现问题时社区的通行做法是向前修复:再写一个反操作迁移,把结构改回期望状态。想靠 repair 追回误操作是救不回来的,必须靠新增迁移。
参数说明:baseline 用在另一个场景——数据库已经运行很久,表结构已经存在,现在才接入 Flyway。baselineOnMigrate=true 时,首次 migrate 会自动创建一条版本 1 的 baseline 记录,把当前数据库状态视为 V1,之后的迁移脚本从 V2 开始执行。这样 Flyway 无需重放历史 DDL,也能接管此后所有结构变更。但要注意:baseline 一旦创建,之前存在的表结构不再受 Flyway 管理,这些表后续需要变更时,必须通过新的迁移脚本补一个基线后迁移。
6. 进阶:用 validate 与 info 把迁移检查做进发布前
6.1 在 CI 流水线里加一个 validate 步骤
Flyway 命令行里的 validate 专门用来校验已执行迁移与本地迁移文件是否一致。在发布流水线里加这一步,能提前发现「有人改了已执行脚本」这类问题,而不是等 deploy 时 migrate 跑到一半才爆出来。
mvn flyway:validate \ -Dflyway.url=jdbc:postgresql://localhost:5432/myapp \ -Dflyway.user=app_user \ -Dflyway.password=secret逻辑说明:validate 不会执行任何迁移,也不会修改数据库,它只是把本地迁移文件的 checksum 与 schema_history 表里存的逐一比对。如果有 mismatch,CI 直接失败,这时候回到 git 历史看是哪个文件被动过。这个步骤非常快,通常几秒内完成,放在构建流程里几乎没有成本。
参数说明:validate 可以配合 ignoreMigrationPatterns 使用,但生产环境不建议放宽——那等于放弃了校验。
6.2 用 info 命令确认发布前的迁移状态
flyway info 显示当前数据库的迁移状态汇总,包括已执行、待执行、失败和忽略的迁移。
mvn flyway:info \ -Dflyway.url=jdbc:postgresql://localhost:5432/myapp \ -Dflyway.user=app_user \ -Dflyway.password=secret输出会列出每个迁移文件的状态列:Pending、Success、Failed、Ignored。发版前看一眼,Pending 状态的应该是你预期带上线的脚本,Failed 状态的必须在跑 migrate 之前处理掉,Ignored 是怎么来的要想清楚——多半是配置文件里写了 ignoreMigrationPatterns,把它去掉看真实状态。
逻辑说明:validate 管校验,info 管状态,两者都不改动数据库,跑起来也很快。我现在的习惯是每次发布前在 CI 里串三个步骤:validate → info → migrate。validate 在前,info 在中,migrate 最后,顺序不能乱。第一次这样跑的时候,多花三十秒,但换回来的是「不会再有一种意外是数据库结构没对齐」。这个习惯陪了我好几个项目,每次发布前的数据库结构问题都能在构建阶段拦下来,而不是等 Jetty 启动失败才回滚发布。希望这个方法也能帮到你少踩几次结构变更的坑。
本文还有配套的精品资源,点击获取