From e789bc0bb7c25ce6db7d48ddb2c231c71c5cfd52 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 11 Oct 2018 15:56:36 +0200 Subject: [PATCH] Use FluentConfiguration to configure Flyway This commit stops binding the Flyway object directly to the environment as mutating it will no longer be supported in Flyway 6. This commit mirrors Flyway's configuration in FlywayProperties for the most part. Closes gh-14776 --- .../flyway/FlywayAutoConfiguration.java | 148 ++++-- .../flyway/FlywayProperties.java | 454 ++++++++++++++++-- ...itional-spring-configuration-metadata.json | 12 +- .../flyway/FlywayPropertiesTests.java | 160 ++++++ .../appendix-application-properties.adoc | 67 ++- .../src/main/asciidoc/howto.adoc | 10 +- 6 files changed, 725 insertions(+), 126 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 288e588d413..f0b24e1e869 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -32,6 +32,7 @@ import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.callback.FlywayCallback; +import org.flywaydb.core.api.configuration.FluentConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -46,9 +47,9 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -61,6 +62,7 @@ import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -76,7 +78,6 @@ import org.springframework.util.StringUtils; * @author Dominic Gunn * @since 1.1.0 */ -@SuppressWarnings("deprecation") @Configuration @ConditionalOnClass(Flyway.class) @ConditionalOnBean(DataSource.class) @@ -97,6 +98,7 @@ public class FlywayAutoConfiguration { return new FlywaySchemaManagementProvider(flyways); } + @SuppressWarnings("deprecation") @Configuration @ConditionalOnMissingBean(Flyway.class) @EnableConfigurationProperties({ DataSourceProperties.class, FlywayProperties.class }) @@ -137,9 +139,18 @@ public class FlywayAutoConfiguration { } @Bean - @ConfigurationProperties(prefix = "spring.flyway") public Flyway flyway() { - Flyway flyway = new SpringBootFlyway(); + FluentConfiguration configuration = new FluentConfiguration(); + DataSource dataSource = configureDataSource(configuration); + checkLocationExists(dataSource); + configureProperties(configuration); + configureCallbacks(configuration); + Flyway flyway = configuration.load(); + configureFlywayCallbacks(flyway); + return flyway; + } + + private DataSource configureDataSource(FluentConfiguration configuration) { if (this.properties.isCreateDataSource()) { String url = getProperty(this.properties::getUrl, this.dataSourceProperties::getUrl); @@ -147,42 +158,25 @@ public class FlywayAutoConfiguration { this.dataSourceProperties::getUsername); String password = getProperty(this.properties::getPassword, this.dataSourceProperties::getPassword); - flyway.setDataSource(url, user, password, - StringUtils.toStringArray(this.properties.getInitSqls())); + configuration.dataSource(url, user, password); + if (!CollectionUtils.isEmpty(this.properties.getInitSqls())) { + String initSql = StringUtils.collectionToDelimitedString( + this.properties.getInitSqls(), "\n"); + configuration.initSql(initSql); + } } else if (this.flywayDataSource != null) { - flyway.setDataSource(this.flywayDataSource); + configuration.dataSource(this.flywayDataSource); } else { - flyway.setDataSource(this.dataSource); - } - if (!this.callbacks.isEmpty() || !this.flywayCallbacks.isEmpty()) { - if (this.flywayCallbacks.isEmpty()) { - flyway.setCallbacks(this.callbacks.toArray(new Callback[0])); - } - else if (this.callbacks.isEmpty()) { - flyway.setCallbacks( - this.flywayCallbacks.toArray(new FlywayCallback[0])); - } - else { - throw new IllegalStateException( - "Found a mixture of Callback and FlywayCallback beans." - + " One type must be used exclusively."); - } + configuration.dataSource(this.dataSource); } - checkLocationExists(flyway); - return flyway; - } - - private String getProperty(Supplier property, - Supplier defaultValue) { - String value = property.get(); - return (value != null) ? value : defaultValue.get(); + return configuration.getDataSource(); } - private void checkLocationExists(Flyway flyway) { + private void checkLocationExists(DataSource dataSource) { if (this.properties.isCheckLocation()) { - String[] locations = new LocationResolver(flyway.getDataSource()) + String[] locations = new LocationResolver(dataSource) .resolveLocations(this.properties.getLocations()); Assert.state(locations.length != 0, "Migration script locations not configured"); @@ -193,6 +187,86 @@ public class FlywayAutoConfiguration { } } + private void configureProperties(FluentConfiguration configuration) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + String[] locations = new LocationResolver(configuration.getDataSource()) + .resolveLocations(this.properties.getLocations()); + map.from(locations).to(configuration::locations); + map.from(this.properties.getEncoding()).to(configuration::encoding); + map.from(this.properties.getConnectRetries()) + .to(configuration::connectRetries); + map.from(this.properties.getSchemas()).as(StringUtils::toStringArray) + .to(configuration::schemas); + map.from(this.properties.getTable()).to(configuration::table); + map.from(this.properties.getBaselineDescription()) + .to(configuration::baselineDescription); + map.from(this.properties.getBaselineVersion()) + .to(configuration::baselineVersion); + map.from(this.properties.getInstalledBy()).to(configuration::installedBy); + map.from(this.properties.getPlaceholders()).to(configuration::placeholders); + map.from(this.properties.getPlaceholderPrefix()) + .to(configuration::placeholderPrefix); + map.from(this.properties.getPlaceholderSuffix()) + .to(configuration::placeholderSuffix); + map.from(this.properties.isPlaceholderReplacement()) + .to(configuration::placeholderReplacement); + map.from(this.properties.getSqlMigrationPrefix()) + .to(configuration::sqlMigrationPrefix); + map.from(this.properties.getSqlMigrationSuffixes()) + .as(StringUtils::toStringArray) + .to(configuration::sqlMigrationSuffixes); + map.from(this.properties.getSqlMigrationSeparator()) + .to(configuration::sqlMigrationSeparator); + map.from(this.properties.getRepeatableSqlMigrationPrefix()) + .to(configuration::repeatableSqlMigrationPrefix); + map.from(this.properties.getTarget()).to(configuration::target); + map.from(this.properties.isBaselineOnMigrate()) + .to(configuration::baselineOnMigrate); + map.from(this.properties.isCleanDisabled()).to(configuration::cleanDisabled); + map.from(this.properties.isCleanOnValidationError()) + .to(configuration::cleanOnValidationError); + map.from(this.properties.isGroup()).to(configuration::group); + map.from(this.properties.isIgnoreMissingMigrations()) + .to(configuration::ignoreMissingMigrations); + map.from(this.properties.isIgnoreIgnoredMigrations()) + .to(configuration::ignoreIgnoredMigrations); + map.from(this.properties.isIgnorePendingMigrations()) + .to(configuration::ignorePendingMigrations); + map.from(this.properties.isIgnoreFutureMigrations()) + .to(configuration::ignoreFutureMigrations); + map.from(this.properties.isMixed()).to(configuration::mixed); + map.from(this.properties.isOutOfOrder()).to(configuration::outOfOrder); + map.from(this.properties.isSkipDefaultCallbacks()) + .to(configuration::skipDefaultCallbacks); + map.from(this.properties.isSkipDefaultResolvers()) + .to(configuration::skipDefaultResolvers); + map.from(this.properties.isValidateOnMigrate()) + .to(configuration::validateOnMigrate); + } + + private void configureCallbacks(FluentConfiguration configuration) { + if (!this.callbacks.isEmpty()) { + configuration.callbacks(this.callbacks.toArray(new Callback[0])); + } + } + + private void configureFlywayCallbacks(Flyway flyway) { + if (!this.flywayCallbacks.isEmpty()) { + if (!this.callbacks.isEmpty()) { + throw new IllegalStateException( + "Found a mixture of Callback and FlywayCallback beans." + + " One type must be used exclusively."); + } + flyway.setCallbacks(this.flywayCallbacks.toArray(new FlywayCallback[0])); + } + } + + private String getProperty(Supplier property, + Supplier defaultValue) { + String value = property.get(); + return (value != null) ? value : defaultValue.get(); + } + private boolean hasAtLeastOneLocation(String... locations) { for (String location : locations) { if (this.resourceLoader.getResource(normalizePrefix(location)).exists()) { @@ -278,16 +352,6 @@ public class FlywayAutoConfiguration { } - private static class SpringBootFlyway extends Flyway { - - @Override - public void setLocations(String... locations) { - super.setLocations( - new LocationResolver(getDataSource()).resolveLocations(locations)); - } - - } - private static class LocationResolver { private static final String VENDOR_PLACEHOLDER = "{vendor}"; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index fce63d348ae..081e9519826 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -16,33 +16,31 @@ package org.springframework.boot.autoconfigure.flyway; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; - -import org.flywaydb.core.Flyway; +import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Configuration properties for Flyway database migrations. These are only the properties - * that Spring needs to validate and enable the migrations. If you want to control the - * location or format of the scripts you can use the same prefix ("flyway") to inject - * properties into the {@link Flyway} instance. + * Configuration properties for Flyway database migrations. * * @author Dave Syer * @author EddĂș MelĂ©ndez + * @author Stephane Nicoll * @since 1.1.0 */ -@ConfigurationProperties(prefix = "spring.flyway", ignoreUnknownFields = true) +@ConfigurationProperties(prefix = "spring.flyway") public class FlywayProperties { /** - * The locations of migrations scripts. Can contain the special "{vendor}" placeholder - * to use vendor-specific locations. + * Whether to enable flyway. */ - private List locations = new ArrayList<>( - Collections.singletonList("classpath:db/migration")); + private boolean enabled = true; /** * Whether to check that migration scripts location exists. @@ -50,19 +48,92 @@ public class FlywayProperties { private boolean checkLocation = true; /** - * Whether to enable flyway. + * Locations of migrations scripts. Can contain the special "{vendor}" placeholder to + * use vendor-specific locations. */ - private boolean enabled = true; + private List locations = new ArrayList<>( + Collections.singletonList("classpath:db/migration")); /** - * Login user of the database to migrate. + * Encoding of SQL migrations. */ - private String user; + private Charset encoding = StandardCharsets.UTF_8; /** - * JDBC password to use if you want Flyway to create its own DataSource. + * Maximum number of retries when attempting to connect to the database. */ - private String password; + private int connectRetries; + + /** + * Scheme names managed by Flyway (case-sensitive). + */ + private List schemas = new ArrayList<>(); + + /** + * Name of the schema schema history table that will be used by Flyway. + */ + private String table = "flyway_schema_history"; + + /** + * Description to tag an existing schema with when applying a baseline. + */ + private String baselineDescription = "<< Flyway Baseline >>"; + + /** + * Version to tag an existing schema with when executing baseline. + */ + private String baselineVersion = "1"; + + /** + * Username recorded in the schema history table as having applied the migration. + */ + private String installedBy; + + /** + * Placeholders and their replacements to apply to sql migration scripts. + */ + private Map placeholders = new HashMap<>(); + + /** + * Prefix of placeholders in migration scripts. + */ + private String placeholderPrefix = "${"; + + /** + * Suffix of placeholders in migration scripts. + */ + private String placeholderSuffix = "}"; + + /** + * Perform placeholder replacement in migration scripts. + */ + private boolean placeholderReplacement = true; + + /** + * File name prefix for SQL migrations. + */ + private String sqlMigrationPrefix = "V"; + + /** + * File name suffix for SQL migrations. + */ + private List sqlMigrationSuffixes = new ArrayList<>( + Collections.singleton(".sql")); + + /** + * File name separator for SQL migrations. + */ + private String sqlMigrationSeparator = "__"; + + /** + * File name prefix for repeatable SQL migrations. + */ + private String repeatableSqlMigrationPrefix = "R"; + + /** + * Target version up to which migrations should be considered. + */ + private String target; /** * JDBC url of the database to migrate. If not set, the primary configured data source @@ -70,50 +141,243 @@ public class FlywayProperties { */ private String url; + /** + * Login user of the database to migrate. + */ + private String user; + + /** + * Login password of the database to migrate. + */ + private String password; + /** * SQL statements to execute to initialize a connection immediately after obtaining * it. */ private List initSqls = new ArrayList<>(); - public void setLocations(List locations) { - this.locations = locations; + /** + * Whether to automatically call baseline when migrating a non-empty schema. + */ + private boolean baselineOnMigrate; + + /** + * Whether to disable cleaning of the database. + */ + private boolean cleanDisabled; + + /** + * Whether to automatically call clean when a validation error occurs. + */ + private boolean cleanOnValidationError; + + /** + * Whether to group all pending migrations together in the same transaction when + * applying them. + */ + private boolean group; + + /** + * Whether to ignore missing migrations when reading the schema history table. + */ + private boolean ignoreMissingMigrations; + + /** + * Whether to ignore ignored migrations when reading the schema history table. + */ + private boolean ignoreIgnoredMigrations; + + /** + * Whether to ignore pending migrations when reading the schema history table. + */ + private boolean ignorePendingMigrations; + + /** + * Whether to ignore future migrations when reading the schema history table. + */ + private boolean ignoreFutureMigrations = true; + + /** + * Whether to allow mixing transactional and non-transactional statements within the + * same migration. + */ + private boolean mixed; + + /** + * Whether to allow migrations to be run out of order. + */ + private boolean outOfOrder; + + /** + * Whether to skip default callbacks. If true, only custom callbacks are used. + */ + private boolean skipDefaultCallbacks; + + /** + * Whether to skip default resolvers. If true, only custom resolvers are used. + */ + private boolean skipDefaultResolvers; + + /** + * Whether to automatically call validate when performing a migration. + */ + private boolean validateOnMigrate = true; + + public boolean isEnabled() { + return this.enabled; } - public List getLocations() { - return this.locations; + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isCheckLocation() { + return this.checkLocation; } public void setCheckLocation(boolean checkLocation) { this.checkLocation = checkLocation; } - public boolean isCheckLocation() { - return this.checkLocation; + public List getLocations() { + return this.locations; } - public boolean isEnabled() { - return this.enabled; + public void setLocations(List locations) { + this.locations = locations; } - public void setEnabled(boolean enabled) { - this.enabled = enabled; + public Charset getEncoding() { + return this.encoding; } - public String getUser() { - return this.user; + public void setEncoding(Charset encoding) { + this.encoding = encoding; } - public void setUser(String user) { - this.user = user; + public int getConnectRetries() { + return this.connectRetries; } - public String getPassword() { - return (this.password != null) ? this.password : ""; + public void setConnectRetries(int connectRetries) { + this.connectRetries = connectRetries; } - public void setPassword(String password) { - this.password = password; + public List getSchemas() { + return this.schemas; + } + + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + public String getTable() { + return this.table; + } + + public void setTable(String table) { + this.table = table; + } + + public String getBaselineDescription() { + return this.baselineDescription; + } + + public void setBaselineDescription(String baselineDescription) { + this.baselineDescription = baselineDescription; + } + + public String getBaselineVersion() { + return this.baselineVersion; + } + + public void setBaselineVersion(String baselineVersion) { + this.baselineVersion = baselineVersion; + } + + public String getInstalledBy() { + return this.installedBy; + } + + public void setInstalledBy(String installedBy) { + this.installedBy = installedBy; + } + + public Map getPlaceholders() { + return this.placeholders; + } + + public void setPlaceholders(Map placeholders) { + this.placeholders = placeholders; + } + + public String getPlaceholderPrefix() { + return this.placeholderPrefix; + } + + public void setPlaceholderPrefix(String placeholderPrefix) { + this.placeholderPrefix = placeholderPrefix; + } + + public String getPlaceholderSuffix() { + return this.placeholderSuffix; + } + + public void setPlaceholderSuffix(String placeholderSuffix) { + this.placeholderSuffix = placeholderSuffix; + } + + public boolean isPlaceholderReplacement() { + return this.placeholderReplacement; + } + + public void setPlaceholderReplacement(boolean placeholderReplacement) { + this.placeholderReplacement = placeholderReplacement; + } + + public String getSqlMigrationPrefix() { + return this.sqlMigrationPrefix; + } + + public void setSqlMigrationPrefix(String sqlMigrationPrefix) { + this.sqlMigrationPrefix = sqlMigrationPrefix; + } + + public List getSqlMigrationSuffixes() { + return this.sqlMigrationSuffixes; + } + + public void setSqlMigrationSuffixes(List sqlMigrationSuffixes) { + this.sqlMigrationSuffixes = sqlMigrationSuffixes; + } + + public String getSqlMigrationSeparator() { + return this.sqlMigrationSeparator; + } + + public void setSqlMigrationSeparator(String sqlMigrationSeparator) { + this.sqlMigrationSeparator = sqlMigrationSeparator; + } + + public String getRepeatableSqlMigrationPrefix() { + return this.repeatableSqlMigrationPrefix; + } + + public void setRepeatableSqlMigrationPrefix(String repeatableSqlMigrationPrefix) { + this.repeatableSqlMigrationPrefix = repeatableSqlMigrationPrefix; + } + + public String getTarget() { + return this.target; + } + + public void setTarget(String target) { + this.target = target; + } + + public boolean isCreateDataSource() { + return this.url != null || this.user != null; } public String getUrl() { @@ -124,6 +388,22 @@ public class FlywayProperties { this.url = url; } + public String getUser() { + return this.user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return (this.password != null) ? this.password : ""; + } + + public void setPassword(String password) { + this.password = password; + } + public List getInitSqls() { return this.initSqls; } @@ -132,8 +412,108 @@ public class FlywayProperties { this.initSqls = initSqls; } - public boolean isCreateDataSource() { - return this.url != null || this.user != null; + public boolean isBaselineOnMigrate() { + return this.baselineOnMigrate; + } + + public void setBaselineOnMigrate(boolean baselineOnMigrate) { + this.baselineOnMigrate = baselineOnMigrate; + } + + public boolean isCleanDisabled() { + return this.cleanDisabled; + } + + public void setCleanDisabled(boolean cleanDisabled) { + this.cleanDisabled = cleanDisabled; + } + + public boolean isCleanOnValidationError() { + return this.cleanOnValidationError; + } + + public void setCleanOnValidationError(boolean cleanOnValidationError) { + this.cleanOnValidationError = cleanOnValidationError; + } + + public boolean isGroup() { + return this.group; + } + + public void setGroup(boolean group) { + this.group = group; + } + + public boolean isIgnoreMissingMigrations() { + return this.ignoreMissingMigrations; + } + + public void setIgnoreMissingMigrations(boolean ignoreMissingMigrations) { + this.ignoreMissingMigrations = ignoreMissingMigrations; + } + + public boolean isIgnoreIgnoredMigrations() { + return this.ignoreIgnoredMigrations; + } + + public void setIgnoreIgnoredMigrations(boolean ignoreIgnoredMigrations) { + this.ignoreIgnoredMigrations = ignoreIgnoredMigrations; + } + + public boolean isIgnorePendingMigrations() { + return this.ignorePendingMigrations; + } + + public void setIgnorePendingMigrations(boolean ignorePendingMigrations) { + this.ignorePendingMigrations = ignorePendingMigrations; + } + + public boolean isIgnoreFutureMigrations() { + return this.ignoreFutureMigrations; + } + + public void setIgnoreFutureMigrations(boolean ignoreFutureMigrations) { + this.ignoreFutureMigrations = ignoreFutureMigrations; + } + + public boolean isMixed() { + return this.mixed; + } + + public void setMixed(boolean mixed) { + this.mixed = mixed; + } + + public boolean isOutOfOrder() { + return this.outOfOrder; + } + + public void setOutOfOrder(boolean outOfOrder) { + this.outOfOrder = outOfOrder; + } + + public boolean isSkipDefaultCallbacks() { + return this.skipDefaultCallbacks; + } + + public void setSkipDefaultCallbacks(boolean skipDefaultCallbacks) { + this.skipDefaultCallbacks = skipDefaultCallbacks; + } + + public boolean isSkipDefaultResolvers() { + return this.skipDefaultResolvers; + } + + public void setSkipDefaultResolvers(boolean skipDefaultResolvers) { + this.skipDefaultResolvers = skipDefaultResolvers; + } + + public boolean isValidateOnMigrate() { + return this.validateOnMigrate; + } + + public void setValidateOnMigrate(boolean validateOnMigrate) { + this.validateOnMigrate = validateOnMigrate; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4b5aaf22d50..581137a8e49 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1440,17 +1440,17 @@ }, { "name": "spring.flyway.locations", - "type": "java.util.List", + "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", "defaultValue": [ "classpath:db/migration" ] }, { - "name": "spring.flyway.sql-migration-suffix", - "deprecation": { - "replacement": "spring.flyway.sql-migration-suffixes", - "level": "warning" - } + "name": "spring.flyway.sql-migration-suffixes", + "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", + "defaultValue": [ + ".sql" + ] }, { "name": "spring.git.properties", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java new file mode 100644 index 00000000000..aec175db2cf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.ClassicConfiguration; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FlywayProperties}. + * + * @author Stephane Nicoll + */ +public class FlywayPropertiesTests { + + @Test + public void defaultValuesAreConsistent() { + FlywayProperties properties = new FlywayProperties(); + Configuration configuration = new FluentConfiguration(); + assertThat(properties.getLocations().stream().map(Location::new) + .toArray(Location[]::new)).isEqualTo(configuration.getLocations()); + assertThat(properties.getEncoding()).isEqualTo(configuration.getEncoding()); + assertThat(properties.getConnectRetries()) + .isEqualTo(configuration.getConnectRetries()); + assertThat(properties.getSchemas()) + .isEqualTo(Arrays.asList(configuration.getSchemas())); + assertThat(properties.getTable()).isEqualTo(configuration.getTable()); + assertThat(properties.getBaselineDescription()) + .isEqualTo(configuration.getBaselineDescription()); + assertThat(MigrationVersion.fromVersion(properties.getBaselineVersion())) + .isEqualTo(configuration.getBaselineVersion()); + assertThat(properties.getInstalledBy()).isEqualTo(configuration.getInstalledBy()); + assertThat(properties.getPlaceholders()) + .isEqualTo(configuration.getPlaceholders()); + assertThat(properties.getPlaceholderPrefix()) + .isEqualToIgnoringWhitespace(configuration.getPlaceholderPrefix()); + assertThat(properties.getPlaceholderSuffix()) + .isEqualTo(configuration.getPlaceholderSuffix()); + assertThat(properties.isPlaceholderReplacement()) + .isEqualTo(configuration.isPlaceholderReplacement()); + assertThat(properties.getSqlMigrationPrefix()) + .isEqualTo(configuration.getSqlMigrationPrefix()); + assertThat(properties.getSqlMigrationSuffixes()) + .isEqualTo(Arrays.asList(configuration.getSqlMigrationSuffixes())); + assertThat(properties.getSqlMigrationSeparator()) + .isEqualTo(properties.getSqlMigrationSeparator()); + assertThat(properties.getRepeatableSqlMigrationPrefix()) + .isEqualTo(properties.getRepeatableSqlMigrationPrefix()); + assertThat(properties.getTarget()).isNull(); + assertThat(configuration.getTarget()).isNull(); + assertThat(configuration.getInitSql()).isNull(); + assertThat(properties.getInitSqls()).isEmpty(); + assertThat(configuration.isBaselineOnMigrate()) + .isEqualTo(properties.isBaselineOnMigrate()); + assertThat(configuration.isCleanDisabled()) + .isEqualTo(properties.isCleanDisabled()); + assertThat(configuration.isCleanOnValidationError()) + .isEqualTo(properties.isCleanOnValidationError()); + assertThat(configuration.isGroup()).isEqualTo(properties.isGroup()); + assertThat(configuration.isIgnoreMissingMigrations()) + .isEqualTo(properties.isIgnoreMissingMigrations()); + assertThat(configuration.isIgnoreIgnoredMigrations()) + .isEqualTo(properties.isIgnoreIgnoredMigrations()); + assertThat(configuration.isIgnorePendingMigrations()) + .isEqualTo(properties.isIgnorePendingMigrations()); + assertThat(configuration.isIgnoreFutureMigrations()) + .isEqualTo(properties.isIgnoreFutureMigrations()); + assertThat(configuration.isMixed()).isEqualTo(properties.isMixed()); + assertThat(configuration.isOutOfOrder()).isEqualTo(properties.isOutOfOrder()); + assertThat(configuration.isSkipDefaultCallbacks()) + .isEqualTo(properties.isSkipDefaultCallbacks()); + assertThat(configuration.isSkipDefaultResolvers()) + .isEqualTo(properties.isSkipDefaultResolvers()); + assertThat(configuration.isValidateOnMigrate()) + .isEqualTo(properties.isValidateOnMigrate()); + } + + @Test + public void expectedPropertiesAreManaged() { + Map properties = indexProperties( + PropertyAccessorFactory.forBeanPropertyAccess(new FlywayProperties())); + Map configuration = indexProperties( + PropertyAccessorFactory + .forBeanPropertyAccess(new ClassicConfiguration())); + // Properties specific settings + ignoreProperties(properties, "url", "user", "password", "enabled", + "checkLocation", "createDataSource"); + + // High level object we can't set with properties + ignoreProperties(configuration, "classLoader", "dataSource", "resolvers", + "callbacks"); + // Properties we don't want to expose + ignoreProperties(configuration, "resolversAsClassNames", "callbacksAsClassNames"); + // Handled by the conversion service + ignoreProperties(configuration, "baselineVersionAsString", "encodingAsString", + "locationsAsStrings", "targetAsString"); + // Handled as initSql array + ignoreProperties(configuration, "initSql"); + ignoreProperties(properties, "initSqls"); + // Pro version only + ignoreProperties(configuration, "batch", "dryRunOutput", "dryRunOutputAsFile", + "dryRunOutputAsFileName", "errorHandlers", "errorHandlersAsClassNames", + "errorOverrides", "licenseKey", "oracleSqlplus", "stream", + "undoSqlMigrationPrefix"); + List configurationKeys = new ArrayList(configuration.keySet()); + Collections.sort(configurationKeys); + List propertiesKeys = new ArrayList(properties.keySet()); + Collections.sort(propertiesKeys); + assertThat(configurationKeys).isEqualTo(propertiesKeys); + } + + private void ignoreProperties(Map index, String... propertyNames) { + for (String propertyName : propertyNames) { + assertThat(index.remove(propertyName)) + .describedAs("Property to ignore should be present " + propertyName) + .isNotNull(); + } + } + + private Map indexProperties(BeanWrapper beanWrapper) { + Map descriptor = new HashMap<>(); + for (PropertyDescriptor propertyDescriptor : beanWrapper + .getPropertyDescriptors()) { + descriptor.put(propertyDescriptor.getName(), propertyDescriptor); + } + ignoreProperties(descriptor, "class"); + return descriptor; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 68ecfde0cba..38ddd637dcf 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -552,47 +552,42 @@ content into your application. Rather, pick only the properties that you need. # ---------------------------------------- # FLYWAY ({sc-spring-boot-autoconfigure}/flyway/FlywayProperties.{sc-ext}[FlywayProperties]) - spring.flyway.baseline-description= # - spring.flyway.baseline-on-migrate= # - spring.flyway.baseline-version=1 # Version to start migration - spring.flyway.batch= # + spring.flyway.baseline-description=<< Flyway Baseline >> # Description to tag an existing schema with when applying a baseline. + spring.flyway.baseline-on-migrate=false # Whether to automatically call baseline when migrating a non-empty schema. + spring.flyway.baseline-version=1 # Version to tag an existing schema with when executing baseline. spring.flyway.check-location=true # Whether to check that migration scripts location exists. - spring.flyway.clean-disabled= # - spring.flyway.clean-on-validation-error= # - spring.flyway.dry-run-output= # + spring.flyway.clean-disabled=false # Whether to disable cleaning of the database. + spring.flyway.clean-on-validation-error=false # Whether to automatically call clean when a validation error occurs. + spring.flyway.connect-retries=0 # Maximum number of retries when attempting to connect to the database. spring.flyway.enabled=true # Whether to enable flyway. - spring.flyway.error-handlers= # - spring.flyway.error-overrides= # - spring.flyway.group= # - spring.flyway.ignore-ignored-migrations= # - spring.flyway.ignore-future-migrations= # - spring.flyway.ignore-missing-migrations= # + spring.flyway.encoding=UTF-8 # Encoding of SQL migrations. + spring.flyway.group=false # Whether to group all pending migrations together in the same transaction when applying them. + spring.flyway.ignore-future-migrations=true # Whether to ignore future migrations when reading the schema history table. + spring.flyway.ignore-ignored-migrations=false # Whether to ignore ignored migrations when reading the schema history table. + spring.flyway.ignore-missing-migrations=false # Whether to ignore missing migrations when reading the schema history table. + spring.flyway.ignore-pending-migrations=false # Whether to ignore pending migrations when reading the schema history table. spring.flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it. - spring.flyway.installed-by= # - spring.flyway.locations=classpath:db/migration # The locations of migrations scripts. - spring.flyway.mixed= # - spring.flyway.oracle-sqlplus= # - spring.flyway.out-of-order= # - spring.flyway.password= # JDBC password to use if you want Flyway to create its own DataSource. - spring.flyway.placeholder-prefix= # - spring.flyway.placeholder-replacement= # - spring.flyway.placeholder-suffix= # - spring.flyway.placeholders.*= # - spring.flyway.repeatable-sql-migration-prefix= # - spring.flyway.schemas= # Schemas to update. - spring.flyway.skip-default-callbacks= # - spring.flyway.skip-default-resolvers= # - spring.flyway.sql-migration-prefix=V # - spring.flyway.sql-migration-separator= # - spring.flyway.sql-migration-suffix=.sql # - spring.flyway.sql-migration-suffixes= # - spring.flyway.stream= # - spring.flyway.table= # - spring.flyway.target= # - spring.flyway.undo-sql-migration-prefix= # + spring.flyway.installed-by= # Username recorded in the schema history table as having applied the migration. + spring.flyway.locations=classpath:db/migration # Locations of migrations scripts. Can contain the special "{vendor}" placeholder to use vendor-specific locations. + spring.flyway.mixed=false # Whether to allow mixing transactional and non-transactional statements within the same migration. + spring.flyway.out-of-order=false # Whether to allow migrations to be run out of order. + spring.flyway.password= # Login password of the database to migrate. + spring.flyway.placeholder-prefix=${ # Prefix of placeholders in migration scripts. + spring.flyway.placeholder-replacement=true # Perform placeholder replacement in migration scripts. + spring.flyway.placeholder-suffix=} # Suffix of placeholders in migration scripts. + spring.flyway.placeholders= # Placeholders and their replacements to apply to sql migration scripts. + spring.flyway.repeatable-sql-migration-prefix=R # File name prefix for repeatable SQL migrations. + spring.flyway.schemas= # Scheme names managed by Flyway (case-sensitive). + spring.flyway.skip-default-callbacks=false # Whether to skip default callbacks. If true, only custom callbacks are used. + spring.flyway.skip-default-resolvers=false # Whether to skip default resolvers. If true, only custom resolvers are used. + spring.flyway.sql-migration-prefix=V # File name prefix for SQL migrations. + spring.flyway.sql-migration-separator=__ # File name separator for SQL migrations. + spring.flyway.sql-migration-suffixes=.sql # File name suffix for SQL migrations. + spring.flyway.table=flyway_schema_history # Name of the schema schema history table that will be used by Flyway. + spring.flyway.target= # Target version up to which migrations should be considered. spring.flyway.url= # JDBC url of the database to migrate. If not set, the primary configured data source is used. spring.flyway.user= # Login user of the database to migrate. - spring.flyway.validate-on-migrate= # + spring.flyway.validate-on-migrate=true # Whether to automatically call validate when performing a migration. # LIQUIBASE ({sc-spring-boot-autoconfigure}/liquibase/LiquibaseProperties.{sc-ext}[LiquibaseProperties]) spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml # Change log configuration path. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc index 4f004e149ab..0dad0de8136 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -2327,11 +2327,11 @@ according to the type of the database (such as `db/migration/mysql` for MySQL). of supported databases is available in {sc-spring-boot}/jdbc/DatabaseDriver.{sc-ext}[`DatabaseDriver`]. -See the Flyway class from flyway-core for details of available settings such as schemas -and others. In addition, Spring Boot provides a small set of properties (in -{sc-spring-boot-autoconfigure}/flyway/FlywayProperties.{sc-ext}[`FlywayProperties`]) -that can be used to disable the migrations or switch off the location checking. Spring -Boot calls `Flyway.migrate()` to perform the database migration. If you would like +{sc-spring-boot-autoconfigure}/flyway/FlywayProperties.{sc-ext}[`FlywayProperties`] +provides most of Flyway's settings and a small set of additional properties that can be +used to disable the migrations or switch off the location checking. + +Spring Boot calls `Flyway.migrate()` to perform the database migration. If you would like more control, provide a `@Bean` that implements {sc-spring-boot-autoconfigure}/flyway/FlywayMigrationStrategy.{sc-ext}[`FlywayMigrationStrategy`].