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`].