Flyway迁移前回调执行异常:如何先修改SQL文件名再执行迁移?
问题分析
你遇到的核心问题是Flyway的生命周期顺序导致beforeMigrate回调时机太晚。Flyway的执行流程是这样的:
- 初始化配置
- 扫描迁移脚本目录,加载所有待执行的脚本到内存
- 对比
schema_version表,确定哪些脚本需要执行 - 触发
beforeMigrate回调 - 执行迁移脚本
- 更新
schema_version表 - 触发
afterMigrate回调
所以你在beforeMigrate里修改文件名时,Flyway已经完成了脚本扫描,内存里存的还是旧文件名的脚本信息,自然会先执行旧脚本;之后你改了文件名,下次启动Flyway又会把新文件名当成未执行的脚本,重复执行导致表已存在的报错。
解决方案
要让Flyway使用修改后的文件名执行迁移,必须在脚本扫描开始前完成文件名修改,有两种可行的方式:
方式1:在Flyway初始化前手动修改磁盘上的文件名
这是最直接的方式,在构建Flyway实例、启动迁移之前,先遍历脚本目录修改目标文件名:
// 1. 先处理迁移脚本文件名修改 File migrationDir = new File("src/main/resources/db/migration"); // 替换成你的迁移目录路径 for (File file : migrationDir.listFiles((dir, name) -> name.equals("V_1__FooScript.sql"))) { String newFileName = "V_05172018__FooScript.sql"; boolean renameSuccess = file.renameTo(new File(migrationDir, newFileName)); if (!renameSuccess) { throw new RuntimeException("Failed to rename migration script"); } } // 2. 再初始化Flyway并执行迁移 Flyway flyway = Flyway.configure() .dataSource("your-jdbc-url", "username", "password") .load(); flyway.migrate();
这种方式会实际修改磁盘上的文件,执行后schema_version表中会记录新的文件名,不会出现重复执行的问题。
方式2:自定义ResourceProvider动态重命名(无磁盘修改)
如果你不想修改磁盘上的实际文件,可以自定义Flyway的ResourceProvider,在脚本加载的过程中动态替换文件名,让Flyway认为脚本的名称是修改后的版本:
第一步:实现自定义ResourceProvider和包装Resource类
import org.flywaydb.core.api.resource.Resource; import org.flywaydb.core.internal.resource.ResourceProvider; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class RenamingResourceProvider implements ResourceProvider { private final ResourceProvider delegate; public RenamingResourceProvider(ResourceProvider delegate) { this.delegate = delegate; } @Override public Collection<Resource> getResources(String location, boolean recursive) { Collection<Resource> originalResources = delegate.getResources(location, recursive); List<Resource> renamedResources = new ArrayList<>(); for (Resource resource : originalResources) { String originalFileName = resource.getFilename(); // 匹配目标脚本,替换文件名 if ("V_1__FooScript.sql".equals(originalFileName)) { renamedResources.add(new RenamingResourceWrapper(resource, "V_05172018__FooScript.sql")); } else { renamedResources.add(resource); } } return renamedResources; } // 包装Resource类,仅重写文件名相关逻辑 private static class RenamingResourceWrapper implements Resource { private final Resource originalResource; private final String newFileName; public RenamingResourceWrapper(Resource originalResource, String newFileName) { this.originalResource = originalResource; this.newFileName = newFileName; } @Override public String getFilename() { return newFileName; } // 其余方法直接委托给原始Resource @Override public InputStream getInputStream() throws IOException { return originalResource.getInputStream(); } @Override public boolean exists() { return originalResource.exists(); } @Override public long size() { return originalResource.size(); } @Override public String getAbsolutePath() { return originalResource.getAbsolutePath(); } @Override public String getRelativePath() { return originalResource.getRelativePath(); } @Override public URI getUri() { return originalResource.getUri(); } @Override public URL getUrl() { return originalResource.getUrl(); } } }
第二步:配置Flyway使用自定义ResourceProvider
Flyway flyway = Flyway.configure() .dataSource("your-jdbc-url", "username", "password") // 用自定义的ResourceProvider包装默认实现 .resourceProvider(new RenamingResourceProvider(Flyway.configure().getResourceProvider())) .load(); flyway.migrate();
这种方式不会修改磁盘上的文件,但Flyway在扫描和执行时会使用你指定的新文件名,schema_version表中也会记录新名称,完美解决你的需求。
额外提示
如果你已经因为之前的操作导致schema_version表中存在旧文件名的记录,需要先手动修改该记录的script字段为新文件名,或者用flyway repair命令修复(注意repair会重新计算校验和,确保脚本内容和记录匹配),否则下次启动还是会重复执行脚本。
内容的提问来源于stack exchange,提问作者Richard
相关产品推荐
相关产品推荐

