# flywayTest **Repository Path**: bluebird_2/flyway-test ## Basic Information - **Project Name**: flywayTest - **Description**: 实验通过flyway达成:a依赖b,b依赖c,a、b、c的SQL脚本需要初始化到不同库里;a依赖b1,b2,a和b1,b2的SQL脚本都初始化到a的库里 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-09-01 - **Last Updated**: 2023-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 此demo是为了实验: a依赖b,b依赖c这种情况下,b和c的初始化脚本能自动执行。这在把b和c当做插件来使用时很有用
参考文章:https://blog.csdn.net/qianzhitu/article/details/110629847 ### 情况一、/condition1: con1-a依赖con1-b,a和b的脚本需要初始化到一个库里(但大多情况应该都是情况二里的混合模式吧,至少按混合模式去支持总好过后面需要改) >测试类:con1/a/src/test/java/org/example/a/Con1AAppTest.java #### 1. 只需要con1-a和con1-b把脚本放在各种项目的同一个目录,即flyway.locations配置的目录 PS:只是这1步骤并不会执行con1-b的脚本 #### 2. 在con1-b里添加FlywayConfig ```java package org.example.b.config; import org.flywaydb.core.Flyway; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class FlywayConfig { @Bean(initMethod = "migrate") public Flyway bFlyway(DataSource dataSource ) { return Flyway.configure() .dataSource(dataSource) .load(); } } ``` #### 3.并且增在con1-b的resources目录下增加META-INF/spring.factories,这样引入con1-b项目就能自动装载FlywayConfig了,这样con1-b下的脚本也能执行到con1-a所配置的数据库中了 ```markdown org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.example.b.config.FlywayConfig ``` #### 4.1. 重要说明:con1-b的脚本文件可千万别跟con1-a重复了,如果名字一毛一样了则最终只有con1-a的脚本执行了。至于脚本执行先后顺序根据实际情况控制脚本版本号大小即可,版本号越小的越先执行。 #### 4.2. 测试了有3个项目,执行顺序 con1-a版本为V1,V4,con1-b版本为V2,V3,V6,con1-c版本为V5,V7,因为它们3个项目的脚本路径是一致的,所以没问题。在下面的混合模式下(既有同数据源(>=3会出问题,<=2m没事),也有不同数据源) >PS:从spring boot2.7开始,慢慢不支持META-INF/spring.factories文件了 需要导入的自动配置类可以放在 /META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中 ### 情况二、/condition2: a依赖b,b依赖c,a、b、c的脚本需要初始化到不同库里;a依赖b1,b2,a和b1,b2的脚本都初始化到a的库里 >测试类:con2/a/src/test/java/org/example/a/Con2AAppTest.java 先说不同数据源的 #### 1.就是多数据源的配置了,b和c基本一样,以b举例
(用其他多数据源方式的比如dynamic-datasource的除1.2基本也能参考) ##### 1.1自己的数据库配置前缀 ```java package org.example.b.config; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @Component @ConfigurationProperties("spring.datasource.b") public class BDataSourceProperties extends DataSourceProperties{ } ``` 为什么要定义一个类继承DataSourceProperties,看源码就知道DataSourceProperties已经定义了@ConfigurationProperties("spring.datasource"),这个标签不能覆盖,直接使用DataSourceProperties的话有前缀的配置值是set不进去的 ##### 1.2定义自己的DataSource ```java package org.example.b.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class BDataSourceConfig { @Qualifier @Bean("bDataSource") public DataSource bDataSource(BDataSourceProperties properties){ return DataSourceBuilder.create() .driverClassName(properties.getDriverClassName()) .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); } } ``` ##### 1.3把自己的DataSource赋给Flyway ```java package org.example.b.config; import org.flywaydb.core.Flyway; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.core.annotation.Order; import javax.annotation.Resource; import javax.sql.DataSource; @Configuration public class BFlywayConfig { @Resource(name = "bDataSource") private DataSource bDataSource; @Bean(initMethod = "migrate",name = "bFlyway") public Flyway bFlyway() { System.out.println("bFlyway初始化"); return Flyway.configure() .dataSource(bDataSource) .locations("classpath:db/migration/b") .load(); } } ``` ##### 1.4在的resources目录下增加META-INF/spring.factories 这样引入b项目就能自动装载FlywayConfig了 spring.factories: ```markdown org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.example.b.config.BDataSourceProperties,\ org.example.b.config.BDataSourceConfig,\ org.example.b.config.BFlywayConfig ``` ##### 1.5记得在DataSource上加@Primary,即项目A里 ##### 1.6在主程序的配置加加a,b,c的数据源配置 ```yaml spring: datasource: type: com.zaxxer.hikari.HikariDataSource a: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/flyway-a?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: root b: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/flyway-b?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: root c: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/flyway-c?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: root ``` >备注:
1、所有的Flyway初始化里一定指定.locations("classpath:db/migration/xxx"),只是单纯配置yml还是会在初始化a时读到b的sql文件,导致flyway报异常,不能成功初始化
2、还是要注意脚本的version不要重复,否则会报异常: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b2Flyway' defined in class path resource [org/example/b2/config/FlywayConfig.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Found more than one migration with version 2 Offenders: -> E:\wonders\myOwnProjects\flywayTest\con2\a\target\classes\db\migration\a\V2__init.sql (SQL) -> E:\wonders\myOwnProjects\flywayTest\con2\b\target\classes\db\migration\b\V2__init.sql (SQL) #### 2. b1、b2的情况跟condition1有2个特殊点导致最终实现上不太一样
##### 特殊点.1 所有初始化Flyway时一定要指定自己的位置.locations("classpath:db/migration/xxx"),否则在b2里没有指定,那么在初始化完b2,去初始化b时会报如下的错 ```markdown org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b2Flyway' defined in class path resource [org/example/b2/config/FlywayConfig.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Found more than one migration with version 2 Offenders: -> E:\wonders\myOwnProjects\flywayTest\con2\b\target\classes\db\migration\b\V1__init.sql (SQL) -> E:\wonders\myOwnProjects\flywayTest\con2\b\target\classes\db\migration\V1__init.sql (SQL) ``` 实际上b下面没有db\migration\V1__init.sql 这个文件,因为在b2没有在初始化Flyway对象时没有指定locations导致b的flyway把locations有了2个路径(执行顺序先b2在b) ##### 特殊点.2 因为b1,b2要跟a初始化到同一个库,a的flyway只是在.locations("classpath:db/migration/a")是不够的,得 .locations("classpath:db/migration/a","classpath:db/migration/b1","classpath:db/migration/b2")这样才行,否则会报错
```markdown Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Detected applied migration not resolved locally: 1 ``` 而且实际情况a、b1、b2的脚本版本号可能会交叉执行,意味着3个bean初始化都需要声明指定3个项目的脚本路径。
所以最终的实现是加入了一个公共依赖模块common-flyway,它提供一个全局注册各项目自己脚本路径的方法,最终在启动项目即a去初始化Flyway即可。 ##### 2.1 common-fly ###### 2.1.1 ```java package org.example.common.flyway; public interface FlywayMigrate { /** * 实际执行 * @return */ public int migrate(); } ``` ###### 2.1.2 ```java package org.example.common.flyway.config; import org.example.common.flyway.FlywayMigrate; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Configuration("flywayInitHelper") public class FlywayInitHelper implements InitializingBean { @Resource private FlywayMigrate flywayMigrate; private static List locations = new ArrayList<>(); public static void addLocations(String... location){ locations.addAll(Arrays.asList(location)); } public static String[] getLocations(){ String[] array = new String[locations.size()]; locations.toArray(array); for (String location: array ) { System.out.println("location:"+location); } return array; } @PostConstruct public void init(){ System.out.println("中转工初始化..."); } @Override public void afterPropertiesSet() throws Exception { flywayMigrate.migrate(); } } ``` ###### 2.1.3 common-flyway/src/main/resources/META-INF/spring.factories ##### 2.2 b1或b2 ###### 2.2.1 ```java package org.example.b1.config; import org.example.common.flyway.config.FlywayInitHelper; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; @Configuration @AutoConfigureBefore({FlywayInitHelper.class}) public class B1FlywayConfig { @PostConstruct public void addLocations() { System.out.println("添加con2-b1的locations"); FlywayInitHelper.addLocations("classpath:db/migration/b1"); } } ``` ###### 2.2.2 con2/b1/src/main/resources/META-INF/spring.factories ``` org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.example.b1.config.B1FlywayConfig ``` ###### 2.2.3 sql脚本 ``` b1 └── src └──resources └──db └──migration └──b1 ├──V1-init.sql └──V4-init.sql b2 └── src └──resources └──db └──migration └──b2 └──V2-init.sql a └── src └──resources └──db └──migration └──a ├──V3-init.sql └──V5-init.sql ``` ##### 2.3 a ###### 2.3.1 ```java package org.example.a.config; import org.example.common.flyway.FlywayMigrate; import org.example.common.flyway.config.FlywayInitHelper; import org.flywaydb.core.Flyway; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.sql.DataSource; @Component public class AFlywayConfig implements FlywayMigrate { @Resource(name = "aDataSource") private DataSource aDataSource; private Flyway flyway; @Override public int migrate() { System.out.println("aFlyway执行脚本"); FlywayInitHelper.addLocations("classpath:db/migration/a"); flyway = Flyway.configure().dataSource(aDataSource) .locations(FlywayInitHelper.getLocations()).load(); return flyway.migrate(); } } ``` ###### 2.3.2 应该注意到了为了解耦和执行顺序保持[b1.addLocations,b2.addLocations,a.addLocations任意],FlywayInitHelper,flyway.migrate();在AFlywayConfig里并没有@Bean去返回Flyway,实际spring用的是flyway自己根据配置文件初始化的。 所以一定要保证b1,b2,a的脚本目录在flyway.locations配置的子目录下。否则会发生脚本都初始化成功了,但是FlywayInitHelper会异常。因为flyway的校验会报异常。 ##### 最后记录一下踩的坑,虽然不是最终方案,记录一下不可行方案备忘: ##### 坑1:后初始化Flyway的地方先获取到已经初始化的flyway对象们获取它们的locations,但是容易导致bean循环依赖。而且也没办法做到a,b1,b2版本的交叉执行,控制3个项目bean初始化的顺序也是问题,@order不起作用,使用 @DependsOn、@AutoConfigureAfter 或 @AutoConfigureBefore违背了即插即拔的初衷 ##### 坑2:让a、b1、b2各自维护自己的版本记录表,但是控制脚本顺序,locations、version还是有影响,费劲也搞不好