Skip to content

Commit 9dc2e09

Browse files
committed
GH-1440 - Revamp application-module-specific Flyway migration to fully isolate modules.
We now set up completely independent migrations per module.
1 parent 361f0e5 commit 9dc2e09

File tree

3 files changed

+69
-44
lines changed

3 files changed

+69
-44
lines changed

spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/flyway/SpringModulithFlywayMigrationStrategy.java

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.flywaydb.core.Flyway;
2222
import org.flywaydb.core.api.Location;
2323
import org.flywaydb.core.api.configuration.Configuration;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
2426
import org.springframework.boot.flyway.autoconfigure.FlywayMigrationStrategy;
2527
import org.springframework.modulith.core.ApplicationModuleIdentifier;
2628
import org.springframework.modulith.core.ApplicationModuleIdentifiers;
@@ -36,6 +38,9 @@
3638
*/
3739
public class SpringModulithFlywayMigrationStrategy implements FlywayMigrationStrategy {
3840

41+
private static final ApplicationModuleIdentifier ROOT = ApplicationModuleIdentifier.of("__root");
42+
private static final Logger LOGGER = LoggerFactory.getLogger(SpringModulithFlywayMigrationStrategy.class);
43+
3944
private final ApplicationModuleIdentifiers identifiers;
4045

4146
/**
@@ -66,45 +71,52 @@ static class SpringModulithFlywayCustomizer {
6671

6772
private final Flyway flyway;
6873

69-
/**
70-
* @param flyway
71-
*/
7274
SpringModulithFlywayCustomizer(Flyway flyway) {
7375
this.flyway = flyway;
7476
}
7577

7678
Stream<Flyway> augment(ApplicationModuleIdentifiers identifiers) {
7779

7880
var configuration = flyway.getConfiguration();
79-
var original = Stream.of(flyway);
81+
return Stream.concat(Stream.of(ROOT), identifiers.stream())
82+
.peek(it -> LOGGER.debug("Executing Flyway migrations for application module {}.", it))
83+
.map(it -> augmentWithApplicationModule(it, configuration));
84+
}
8085

81-
if (Stream.of(configuration.getLocations()).map(Location::toString).anyMatch(it -> it.endsWith("*"))) {
82-
return original;
83-
}
86+
private Flyway augmentWithApplicationModule(ApplicationModuleIdentifier identifier,
87+
Configuration configuration) {
8488

85-
var augmented = identifiers.stream()
86-
.map(it -> augmentWithApplicationModule(it, configuration))
87-
.map(it -> withNewLocation(configuration, it));
89+
var locations = Stream.of(configuration.getLocations())
90+
.map(Location::toString)
91+
.map(it -> customizeLocation(it, identifier))
92+
.toList();
8893

89-
return Stream.concat(original, augmented);
94+
return withNewLocation(configuration, locations, identifier);
9095
}
9196

92-
private List<String> augmentWithApplicationModule(ApplicationModuleIdentifier identifier,
93-
Configuration configuration) {
97+
private static String customizeLocation(String location, ApplicationModuleIdentifier identifier) {
98+
99+
if (location.endsWith("*")) {
100+
return location;
101+
}
94102

95103
var asPath = identifier.toString().replace('.', '/');
96104

97-
return Stream.of(configuration.getLocations())
98-
.map(Location::toString)
99-
.map(it -> it.concat("/").concat(asPath))
100-
.toList();
105+
return location + "/" + asPath;
101106
}
102107

103-
private static Flyway withNewLocation(Configuration configuration, List<String> locations) {
108+
private static Flyway withNewLocation(Configuration configuration, List<String> locations,
109+
ApplicationModuleIdentifier identifier) {
110+
111+
var table = configuration.getTable();
112+
var customizedTable = identifier.equals(ROOT) ? table : table + "_" + identifier;
104113

105114
return Flyway.configure()
106115
.configuration(configuration)
107116
.locations(locations.toArray(String[]::new))
117+
.table(customizedTable)
118+
.baselineVersion("0")
119+
.baselineOnMigrate(true)
108120
.load();
109121
}
110122
}

spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/flyway/SpringModulithFlywayCustomizerUnitTests.java

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public class SpringModulithFlywayCustomizerUnitTests {
4646
@TestFactory // GH-1067
4747
Stream<DynamicTest> augmentsLocationWithApplicationModuleIdentifier() {
4848

49-
var expected = List.of(List.of("classpath:db/migration/first"), List.of("classpath:db/migration/second"));
49+
var expected = List.of(
50+
List.of("classpath:db/migration/first"),
51+
List.of("classpath:db/migration/second"));
5052

5153
var twoIdentifiers = List.of("first", "second");
5254
var singleIdentifier = List.of("identifier");
@@ -58,10 +60,12 @@ Stream<DynamicTest> augmentsLocationWithApplicationModuleIdentifier() {
5860
var mixedLocation = List.of("classpath:db/foo", "classpath:db/migration/**");
5961

6062
var first = new Fixture(twoIdentifiers, noLocation, expected);
61-
var second = new Fixture(singleIdentifier, wildcardLocation, List.of(wildcardLocation));
63+
var second = new Fixture(singleIdentifier, wildcardLocation,
64+
List.of(List.of("classpath:db/migration/**")));
6265
var third = new Fixture(singleIdentifier, multipleLocations,
6366
List.of(List.of("classpath:db/first/identifier", "classpath:db/second/identifier")));
64-
var fourth = new Fixture(singleIdentifier, mixedLocation, List.of(mixedLocation));
67+
var fourth = new Fixture(singleIdentifier, mixedLocation,
68+
List.of(List.of("classpath:db/foo/identifier", "classpath:db/migration/**")));
6569
var fifth = new Fixture(fullyQualifiedIdentifier, noLocation,
6670
List.of(List.of("classpath:db/migration/fully/qualified")));
6771

@@ -93,33 +97,36 @@ public void execute() throws Throwable {
9397
var result = new SpringModulithFlywayCustomizer(configure.load())
9498
.augment(createIdentifiers(identifiers));
9599

96-
var numberOfFlywayInstances = hasWildCardLocation() ? 1 : identifiers.size() + 1;
100+
var numberOfFlywayInstances = identifiers.size() + 1;
101+
var expectedRootLocations = locations.isEmpty()
102+
? List.of("classpath:db/migration/__root")
103+
: locations.stream()
104+
.map(it -> it.endsWith("*") ? it : it + "/__root")
105+
.toList();
97106

98-
assertThat(result).hasSize(numberOfFlywayInstances).satisfies(it -> {
107+
assertThat(result)
108+
.hasSize(numberOfFlywayInstances)
109+
.satisfies(it -> {
99110

100-
// Keeps original migration first
111+
// Keeps original migration first
101112

102-
assertThat(it).element(0)
103-
.extracting(Flyway::getConfiguration)
104-
.extracting(Configuration::getLocations, as(InstanceOfAssertFactories.ARRAY))
105-
.extracting(Object::toString)
106-
.containsAll(locations.isEmpty() ? List.of("classpath:db/migration") : locations);
113+
assertThat(it).element(0)
114+
.extracting(Flyway::getConfiguration)
115+
.extracting(Configuration::getLocations, as(InstanceOfAssertFactories.ARRAY))
116+
.extracting(Object::toString)
117+
.containsAll(expectedRootLocations);
107118

108-
// Has added expected additional migrations
119+
// Has added expected additional migrations
109120

110-
for (int i = 1; i < expected.size(); i++) {
121+
for (int i = 0; i < expected.size(); i++) {
111122

112-
var customized = it.get(i);
123+
var customized = it.get(i + 1);
113124

114-
assertThat(customized.getConfiguration().getLocations())
115-
.extracting(Location::toString)
116-
.containsAll(expected.get(i - 1));
117-
}
118-
});
119-
}
120-
121-
private boolean hasWildCardLocation() {
122-
return locations.stream().anyMatch(it -> it.endsWith("*"));
125+
assertThat(customized.getConfiguration().getLocations())
126+
.extracting(Location::toString)
127+
.containsAll(expected.get(i));
128+
}
129+
});
123130
}
124131

125132
private static ApplicationModuleIdentifiers createIdentifiers(List<String> identifiers) {

src/docs/antora/modules/ROOT/pages/runtime.adoc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,14 @@ As of Spring Modulith 2.0, we support the execution of module-specific https://d
113113
Application modules are encouraged to define migrations for their own persistent data only, which means that these migrations have to be executed in the order of the module dependency tree.
114114

115115
Assume a default Flyway setup with migrations located in `classpath:db/migration`, two application modules `first` and `second` (with `second` depending on `first`), and an activated `spring.modulith.runtime.flyway-enabled` xref:appendix.adoc#configuration-properties[configuration property].
116-
With that in place we will augment the configured migration to run the default one first, followed by one for `classpath:db/migration/first` and one for `classpath:db/migration/second`.
117116

118-
By selecting which folder to place the migration files in, you can differentiate migrations always to be run from ones that will only get executed for the corresponding module. The application module test integration will only execute the default migration and the ones for modules included in the test run.
117+
With that in place we will customize the Flyway setup as follows:
118+
119+
- The root migration folder will be changed to `db/migration/__root`. For that, the default version tracking table will be used.
120+
- Additional migrations for `db/migration/${moduleIdentifier}` will be registered with a tracking table of `flyway_schema_history_${moduleIdentifier}`.
121+
These migrations are also set up to a baseline version of 0 and set up to baseline on migrate.
122+
- Migration locations ending in a wildcard will not be customized.
119123

120-
We will not augment migration runs including wildcard expressions in the migration location definition.
124+
Note, that he version numbers used in migration scripts are now effectively scoped to the application module and should not use global ordering.
125+
126+
By selecting which folder to place the migration files in, you can differentiate migrations always to be run from ones that will only get executed for the corresponding module. The application module test integration will only execute the default migration and the ones for modules included in the test run.

0 commit comments

Comments
 (0)