Auto-configure script-based R2DBC database initialization

See gh-24741
This commit is contained in:
Andy Wilkinson
2021-03-26 17:31:44 +00:00
parent 9cc7f0b54d
commit e49a88cb9b
10 changed files with 311 additions and 118 deletions

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2021 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
*
* https://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.sql.init;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.StringUtils;
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
class DataSourceInitializationConfiguration {
@Bean
DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) {
DatabaseInitializationSettings settings = SettingsCreator.createFrom(initializationProperties);
return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource,
initializationProperties.getUsername(), initializationProperties.getPassword()), settings);
}
private static DataSource determineDataSource(DataSource dataSource, String username, String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
DataSourceBuilder.derivedFrom(dataSource).username(username).password(password)
.type(SimpleDriverDataSource.class).build();
}
return dataSource;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2021 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
*
* https://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.sql.init;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.r2dbc.ConnectionFactoryBuilder;
import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
/**
* Configuration for initializing an SQL database accessed via an R2DBC
* {@link ConnectionFactory}.
*
* @author Andy Wilkinson
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ConnectionFactory.class)
@ConditionalOnSingleCandidate(ConnectionFactory.class)
class R2dbcInitializationConfiguration {
@Bean
R2dbcScriptDatabaseInitializer r2dbcScriptDatabaseInitializer(ConnectionFactory connectionFactory,
SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = SettingsCreator.createFrom(properties);
return new R2dbcScriptDatabaseInitializer(
determineConnectionFactory(connectionFactory, properties.getUsername(), properties.getPassword()),
settings);
}
private static ConnectionFactory determineConnectionFactory(ConnectionFactory connectionFactory, String username,
String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
ConnectionFactoryBuilder.derivefrom(connectionFactory).username(username).password(password).build();
}
return connectionFactory;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2021 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
*
* https://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.sql.init;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
/**
* Helpers class for creating {@link DatabaseInitializationSettings} from
* {@link SqlInitializationProperties}.
*
* @author Andy Wilkinson
*/
final class SettingsCreator {
private SettingsCreator() {
}
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(
scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getEncoding());
return settings;
}
private static List<String> scriptLocations(List<String> locations, String fallback, String platform) {
if (locations != null) {
return locations;
}
List<String> fallbackLocations = new ArrayList<>();
fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql");
fallbackLocations.add("optional:classpath*:" + fallback + ".sql");
return fallbackLocations;
}
}

View File

@@ -16,27 +16,16 @@
package org.springframework.boot.autoconfigure.sql.init;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for initializing an SQL database.
@@ -45,49 +34,11 @@ import org.springframework.util.StringUtils;
* @since 2.5.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnProperty(prefix = "spring.sql.init", name = "enabled", matchIfMissing = true)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureAfter({ R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class })
@EnableConfigurationProperties(SqlInitializationProperties.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
@Import({ R2dbcInitializationConfiguration.class, DataSourceInitializationConfiguration.class })
public class SqlInitializationAutoConfiguration {
@Bean
DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) {
DatabaseInitializationSettings settings = createSettings(initializationProperties);
return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource,
initializationProperties.getUsername(), initializationProperties.getPassword()), settings);
}
private static DatabaseInitializationSettings createSettings(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(
scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getEncoding());
return settings;
}
private static List<String> scriptLocations(List<String> locations, String fallback, String platform) {
if (locations != null) {
return locations;
}
List<String> fallbackLocations = new ArrayList<>();
fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql");
fallbackLocations.add("optional:classpath*:" + fallback + ".sql");
return fallbackLocations;
}
private static DataSource determineDataSource(DataSource dataSource, String username, String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
DataSourceBuilder.derivedFrom(dataSource).username(username).password(password)
.type(SimpleDriverDataSource.class).build();
}
return dataSource;
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2012-2021 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
*
* https://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.sql.init;
import java.nio.charset.Charset;
import java.util.List;
import javax.sql.DataSource;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SqlInitializationAutoConfiguration}.
*
* @author Andy Wilkinson
*/
public class SqlInitializationAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)).withPropertyValues(
"spring.datasource.generate-unique-name:true", "spring.r2dbc.generate-unique-name:true");
@Test
void whenNoDataSourceOrConnectionFactoryIsAvailableThenAutoConfigurationBacksOff() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenConnectionFactoryIsAvailableThenR2dbcInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(R2dbcScriptDatabaseInitializer.class));
}
@Test
void whenConnectionFactoryIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withPropertyValues("spring.sql.init.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceIsAvailableThenDataSourceInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(DataSourceScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
.withPropertyValues("spring.sql.init.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceAndConnectionFactoryAreAvailableThenOnlyR2dbcInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withUserConfiguration(DataSourceAutoConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class)
.hasSingleBean(DataSource.class).hasSingleBean(R2dbcScriptDatabaseInitializer.class)
.doesNotHaveBean(DataSourceScriptDatabaseInitializer.class));
}
@Test
void whenAnInitializerIsDefinedThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withUserConfiguration(DataSourceAutoConfiguration.class, DatabaseInitializerConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(AbstractScriptDatabaseInitializer.class)
.hasBean("customInitializer"));
}
@Configuration(proxyBeanMethods = false)
static class DatabaseInitializerConfiguration {
@Bean
AbstractScriptDatabaseInitializer customInitializer() {
return new AbstractScriptDatabaseInitializer(new DatabaseInitializationSettings()) {
@Override
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator,
Charset encoding) {
// No-op
}
};
}
}
}