# 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还是有影响,费劲也搞不好