Generalize script-based DB initialization and add R2DBC initializer

See gh-24741
This commit is contained in:
Andy Wilkinson 2021-03-26 15:14:47 +00:00
parent eb1200415d
commit 9cc7f0b54d
16 changed files with 361 additions and 112 deletions

View File

@ -32,8 +32,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.init.DataSourceInitializationSettings; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.ScriptDataSourceInitializer; import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -148,9 +148,10 @@ class LiquibaseEndpointTests {
DataSource dataSource = new EmbeddedDatabaseBuilder() DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseConnection.get(getClass().getClassLoader()).getType()) .setType(EmbeddedDatabaseConnection.get(getClass().getClassLoader()).getType())
.setName(UUID.randomUUID().toString()).build(); .setName(UUID.randomUUID().toString()).build();
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("classpath:/db/create-custom-schema.sql")); settings.setSchemaLocations(Arrays.asList("classpath:/db/create-custom-schema.sql"));
ScriptDataSourceInitializer initializer = new ScriptDataSourceInitializer(dataSource, settings); DataSourceScriptDatabaseInitializer initializer = new DataSourceScriptDatabaseInitializer(dataSource,
settings);
initializer.initializeDatabase(); initializer.initializeDatabase();
return dataSource; return dataSource;
} }

View File

@ -41,9 +41,9 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationConfi
import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.init.DataSourceInitializationSettings; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.ScriptDataSourceInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer; import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
@ -56,7 +56,7 @@ import org.springframework.util.StringUtils;
/** /**
* Configuration for {@link DataSource} initialization using a * Configuration for {@link DataSource} initialization using a
* {@link ScriptDataSourceInitializer} with DDL and DML scripts. * {@link DataSourceScriptDatabaseInitializer} with DDL and DML scripts.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@ -87,13 +87,13 @@ class DataSourceInitializationConfiguration {
@org.springframework.context.annotation.Conditional(DifferentCredentialsCondition.class) @org.springframework.context.annotation.Conditional(DifferentCredentialsCondition.class)
@org.springframework.context.annotation.Import(DataSourceInitializationDependencyConfigurer.class) @org.springframework.context.annotation.Import(DataSourceInitializationDependencyConfigurer.class)
@ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(ScriptDataSourceInitializer.class) @ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class)
static class InitializationSpecificCredentialsDataSourceInitializationConfiguration { static class InitializationSpecificCredentialsDataSourceInitializationConfiguration {
@Bean @Bean
ScriptDataSourceInitializer ddlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource, DataSourceScriptDatabaseInitializer ddlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource,
DataSourceProperties properties, ResourceLoader resourceLoader) { DataSourceProperties properties, ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform())); settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError()); settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator()); settings.setSeparator(properties.getSeparator());
@ -106,9 +106,9 @@ class DataSourceInitializationConfiguration {
@Bean @Bean
@DependsOn("ddlOnlyScriptDataSourceInitializer") @DependsOn("ddlOnlyScriptDataSourceInitializer")
ScriptDataSourceInitializer dmlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource, DataSourceScriptDatabaseInitializer dmlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource,
DataSourceProperties properties, ResourceLoader resourceLoader) { DataSourceProperties properties, ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform())); settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError()); settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator()); settings.setSeparator(properties.getSeparator());
@ -144,13 +144,13 @@ class DataSourceInitializationConfiguration {
@org.springframework.context.annotation.Import(DataSourceInitializationDependencyConfigurer.class) @org.springframework.context.annotation.Import(DataSourceInitializationDependencyConfigurer.class)
@org.springframework.context.annotation.Conditional(DataSourceInitializationCondition.class) @org.springframework.context.annotation.Conditional(DataSourceInitializationCondition.class)
@ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(ScriptDataSourceInitializer.class) @ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class)
static class SharedCredentialsDataSourceInitializationConfiguration { static class SharedCredentialsDataSourceInitializationConfiguration {
@Bean @Bean
ScriptDataSourceInitializer scriptDataSourceInitializer(DataSource dataSource, DataSourceProperties properties, DataSourceScriptDatabaseInitializer scriptDataSourceInitializer(DataSource dataSource,
ResourceLoader resourceLoader) { DataSourceProperties properties, ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform())); settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform())); settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError()); settings.setContinueOnError(properties.isContinueOnError());
@ -188,12 +188,12 @@ class DataSourceInitializationConfiguration {
} }
static class InitializationModeDataSourceScriptDatabaseInitializer extends ScriptDataSourceInitializer { static class InitializationModeDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
private final DataSourceInitializationMode mode; private final DataSourceInitializationMode mode;
InitializationModeDataSourceScriptDatabaseInitializer(DataSource dataSource, InitializationModeDataSourceScriptDatabaseInitializer(DataSource dataSource,
DataSourceInitializationSettings settings, DataSourceInitializationMode mode) { DatabaseInitializationSettings settings, DataSourceInitializationMode mode) {
super(dataSource, settings); super(dataSource, settings);
this.mode = mode; this.mode = mode;
} }

View File

@ -29,9 +29,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandi
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.init.DataSourceInitializationSettings; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.ScriptDataSourceInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer; import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -45,7 +45,7 @@ import org.springframework.util.StringUtils;
* @since 2.5.0 * @since 2.5.0
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ScriptDataSourceInitializer.class) @ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnProperty(prefix = "spring.sql.init", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "spring.sql.init", name = "enabled", matchIfMissing = true)
@AutoConfigureAfter(DataSourceAutoConfiguration.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class)
@ -54,15 +54,15 @@ import org.springframework.util.StringUtils;
public class SqlInitializationAutoConfiguration { public class SqlInitializationAutoConfiguration {
@Bean @Bean
ScriptDataSourceInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) { SqlInitializationProperties initializationProperties) {
DataSourceInitializationSettings settings = createSettings(initializationProperties); DatabaseInitializationSettings settings = createSettings(initializationProperties);
return new ScriptDataSourceInitializer(determineDataSource(dataSource, initializationProperties.getUsername(), return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource,
initializationProperties.getPassword()), settings); initializationProperties.getUsername(), initializationProperties.getPassword()), settings);
} }
private static DataSourceInitializationSettings createSettings(SqlInitializationProperties properties) { private static DatabaseInitializationSettings createSettings(SqlInitializationProperties properties) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations( settings.setSchemaLocations(
scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform())); scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform())); settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform()));

View File

@ -43,7 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.init.ScriptDataSourceInitializer; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.dependency.DependsOnDataSourceInitialization; import org.springframework.boot.jdbc.init.dependency.DependsOnDataSourceInitialization;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
@ -240,14 +240,15 @@ class DataSourceAutoConfigurationTests {
void testDataSourceIsInitializedEarly() { void testDataSourceIsInitializedEarly() {
this.contextRunner.withUserConfiguration(TestInitializedDataSourceConfiguration.class) this.contextRunner.withUserConfiguration(TestInitializedDataSourceConfiguration.class)
.withPropertyValues("spring.datasource.initialization-mode=always").run((context) -> { .withPropertyValues("spring.datasource.initialization-mode=always").run((context) -> {
assertThat(context).hasSingleBean(ScriptDataSourceInitializer.class); assertThat(context).hasSingleBean(DataSourceScriptDatabaseInitializer.class);
assertThat(context.getBean(TestInitializedDataSourceConfiguration.class).called).isTrue(); assertThat(context.getBean(TestInitializedDataSourceConfiguration.class).called).isTrue();
}); });
} }
@Test @Test
void whenNoInitializationRelatedSpringDataSourcePropertiesAreConfiguredThenInitializationBacksOff() { void whenNoInitializationRelatedSpringDataSourcePropertiesAreConfiguredThenInitializationBacksOff() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ScriptDataSourceInitializer.class)); this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(DataSourceScriptDatabaseInitializer.class));
} }
private static Function<ApplicationContextRunner, ApplicationContextRunner> hideConnectionPools() { private static Function<ApplicationContextRunner, ApplicationContextRunner> hideConnectionPools() {

View File

@ -0,0 +1,75 @@
/*
* 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.jdbc.init;
import java.nio.charset.Charset;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using schema
* (DDL) and data (DML) scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DataSourceScriptDatabaseInitializer extends AbstractScriptDatabaseInitializer {
private final DataSource dataSource;
/**
* Creates a new {@link DataSourceScriptDatabaseInitializer} that will initialize the
* given {@code DataSource} using the given settings.
* @param dataSource data source to initialize
* @param settings initialization settings
*/
public DataSourceScriptDatabaseInitializer(DataSource dataSource, DatabaseInitializationSettings settings) {
super(settings);
this.dataSource = dataSource;
}
/**
* Returns the {@code DataSource} that will be initialized.
* @return the initialization data source
*/
protected final DataSource getDataSource() {
return this.dataSource;
}
@Override
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator, Charset encoding) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setContinueOnError(continueOnError);
populator.setSeparator(separator);
if (encoding != null) {
populator.setSqlScriptEncoding(encoding.name());
}
for (Resource resource : resources) {
populator.addScript(resource);
}
DatabasePopulatorUtils.execute(populator, this.dataSource);
}
}

View File

@ -23,15 +23,16 @@ import org.springframework.boot.jdbc.init.dependency.AbstractBeansOfTypeDataSour
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector; import org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector;
/** /**
* A {@link DataSourceInitializerDetector} for {@link ScriptDataSourceInitializer}. * A {@link DataSourceInitializerDetector} for
* {@link DataSourceScriptDatabaseInitializer}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class ScriptDataSourceInitializerDetector extends AbstractBeansOfTypeDataSourceInitializerDetector { class DataSourceScriptDatabaseInitializerDetector extends AbstractBeansOfTypeDataSourceInitializerDetector {
@Override @Override
protected Set<Class<?>> getDataSourceInitializerBeanTypes() { protected Set<Class<?>> getDataSourceInitializerBeanTypes() {
return Collections.singleton(ScriptDataSourceInitializer.class); return Collections.singleton(DataSourceScriptDatabaseInitializer.class);
} }
} }

View File

@ -15,6 +15,7 @@
*/ */
/** /**
* Support for initializaton of a JDBC {@code DataSource}. * Support for initializaton of an SQL database using a JDBC {@link javax.sql.DataSource
* DataSource}.
*/ */
package org.springframework.boot.jdbc.init; package org.springframework.boot.jdbc.init;

View File

@ -0,0 +1,68 @@
/*
* 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.r2dbc.init;
import java.nio.charset.Charset;
import java.util.List;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.core.io.Resource;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
/**
* An {@link InitializingBean} that initializes a database represented by an R2DBC
* {@link ConnectionFactory}.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class R2dbcScriptDatabaseInitializer extends AbstractScriptDatabaseInitializer {
private final ConnectionFactory connectionFactory;
/**
* Creates a new {@code R2dbcScriptDatabaseInitializer} that will initialize the
* database recognized by the given {@code connectionFactory} using the given
* {@code settings}.
* @param connectionFactory connectionFactory for the database
* @param settings initialization settings
*/
public R2dbcScriptDatabaseInitializer(ConnectionFactory connectionFactory,
DatabaseInitializationSettings settings) {
super(settings);
this.connectionFactory = connectionFactory;
}
@Override
protected void runScripts(List<Resource> scripts, boolean continueOnError, String separator, Charset encoding) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setContinueOnError(continueOnError);
populator.setSeparator(separator);
if (encoding != null) {
populator.setSqlScriptEncoding(encoding.name());
}
for (Resource script : scripts) {
populator.addScript(script);
}
populator.populate(this.connectionFactory).block();
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Support for initializaton of an SQL database using an R2DBC
* {@link io.r2dbc.spi.ConnectionFactory ConnectionFactory}.
*/
package org.springframework.boot.r2dbc.init;

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.jdbc.init; package org.springframework.boot.sql.init;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -23,54 +23,38 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware; import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
/** /**
* {@link InitializingBean} that performs {@link DataSource} initialization using schema * Base class for an {@link InitializingBean} that performs SQL database initialization
* (DDL) and data (DML) scripts. * using schema (DDL) and data (DML) scripts.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 2.5.0 * @since 2.5.0
*/ */
public class ScriptDataSourceInitializer implements ResourceLoaderAware, InitializingBean { public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
private static final String OPTIONAL_LOCATION_PREFIX = "optional:"; private static final String OPTIONAL_LOCATION_PREFIX = "optional:";
private final DataSource dataSource; private final DatabaseInitializationSettings settings;
private final DataSourceInitializationSettings settings;
private volatile ResourceLoader resourceLoader; private volatile ResourceLoader resourceLoader;
/** /**
* Creates a new {@link ScriptDataSourceInitializer} that will initialize the given * Creates a new {@link AbstractScriptDatabaseInitializer} that will initialize the
* {@code DataSource} using the given settings. * database using the given settings.
* @param dataSource data source to initialize
* @param settings initialization settings * @param settings initialization settings
*/ */
public ScriptDataSourceInitializer(DataSource dataSource, DataSourceInitializationSettings settings) { protected AbstractScriptDatabaseInitializer(DatabaseInitializationSettings settings) {
this.dataSource = dataSource;
this.settings = settings; this.settings = settings;
} }
/**
* Returns the {@code DataSource} that will be initialized.
* @return the initialization data source
*/
protected final DataSource getDataSource() {
return this.dataSource;
}
@Override @Override
public void setResourceLoader(ResourceLoader resourceLoader) { public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader; this.resourceLoader = resourceLoader;
@ -148,18 +132,8 @@ public class ScriptDataSourceInitializer implements ResourceLoaderAware, Initial
this.settings.getEncoding()); this.settings.getEncoding());
} }
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator, Charset encoding) { protected abstract void runScripts(List<Resource> resources, boolean continueOnError, String separator,
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); Charset encoding);
populator.setContinueOnError(continueOnError);
populator.setSeparator(separator);
if (encoding != null) {
populator.setSqlScriptEncoding(encoding.name());
}
for (Resource resource : resources) {
populator.addScript(resource);
}
DatabasePopulatorUtils.execute(populator, this.dataSource);
}
private static class ScriptLocationResolver { private static class ScriptLocationResolver {

View File

@ -14,20 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.jdbc.init; package org.springframework.boot.sql.init;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import javax.sql.DataSource;
/** /**
* Settings for initializing a database using a JDBC {@link DataSource}. * Settings for initializing an SQL database.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 2.5.0 * @since 2.5.0
*/ */
public class DataSourceInitializationSettings { public class DatabaseInitializationSettings {
private List<String> schemaLocations; private List<String> schemaLocations;

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Support for initializaton of an SQL database.
*/
package org.springframework.boot.sql.init;

View File

@ -82,7 +82,7 @@ org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter
# DataSource Initializer Detectors # DataSource Initializer Detectors
org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector=\ org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector=\
org.springframework.boot.flyway.FlywayDataSourceInitializerDetector,\ org.springframework.boot.flyway.FlywayDataSourceInitializerDetector,\
org.springframework.boot.jdbc.init.ScriptDataSourceInitializerDetector,\ org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializerDetector,\
org.springframework.boot.liquibase.LiquibaseDataSourceInitializerDetector,\ org.springframework.boot.liquibase.LiquibaseDataSourceInitializerDetector,\
org.springframework.boot.orm.jpa.JpaDataSourceInitializerDetector org.springframework.boot.orm.jpa.JpaDataSourceInitializerDetector

View File

@ -0,0 +1,55 @@
/*
* 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.jdbc.init;
import java.util.UUID;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.AfterEach;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializerTests;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Tests for {@link DataSourceScriptDatabaseInitializer}.
*
* @author Andy Wilkinson
*/
class DataSourceScriptDatabaseInitializerTests extends AbstractScriptDatabaseInitializerTests {
private final HikariDataSource dataSource = DataSourceBuilder.create().type(HikariDataSource.class)
.url("jdbc:h2:mem:" + UUID.randomUUID()).build();
@AfterEach
void closeDataSource() {
this.dataSource.close();
}
@Override
protected AbstractScriptDatabaseInitializer createInitializer(DatabaseInitializationSettings settings) {
return new DataSourceScriptDatabaseInitializer(this.dataSource, settings);
}
@Override
protected int numberOfRows(String sql) {
return new JdbcTemplate(this.dataSource).queryForObject(sql, Integer.class);
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.r2dbc.init;
import java.util.UUID;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.r2dbc.ConnectionFactoryBuilder;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializerTests;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.r2dbc.core.DatabaseClient;
/**
* Tests for {@link R2dbcScriptDatabaseInitializer}.
*
* @author Andy Wilkinson
*/
class R2dbcScriptDatabaseInitializerTests extends AbstractScriptDatabaseInitializerTests {
private final ConnectionFactory connectionFactory = ConnectionFactoryBuilder
.withUrl("r2dbc:h2:mem:///" + UUID.randomUUID() + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
.build();
@Override
protected AbstractScriptDatabaseInitializer createInitializer(DatabaseInitializationSettings settings) {
return new R2dbcScriptDatabaseInitializer(this.connectionFactory, settings);
}
@Override
protected int numberOfRows(String sql) {
return DatabaseClient.create(this.connectionFactory).sql(sql).map((row, metadata) -> row.get(0)).first()
.map((number) -> ((Number) number).intValue()).block();
}
}

View File

@ -14,105 +14,88 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.jdbc.init; package org.springframework.boot.sql.init;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/** /**
* Tests for {@link ScriptDataSourceInitializer}. * Base class for testing {@link AbstractScriptDatabaseInitializer} implementations.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class ScriptDataSourceInitializerTests { public abstract class AbstractScriptDatabaseInitializerTests {
private final HikariDataSource dataSource = DataSourceBuilder.create().type(HikariDataSource.class)
.url("jdbc:h2:mem:" + UUID.randomUUID()).build();
@AfterEach
void closeDataSource() {
this.dataSource.close();
}
@Test @Test
void whenDatabaseIsInitializedThenSchemaAndDataScriptsAreApplied() { void whenDatabaseIsInitializedThenSchemaAndDataScriptsAreApplied() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql")); settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql")); settings.setDataLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue(); assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfRows("SELECT COUNT(*) FROM EXAMPLE")).isEqualTo(1); assertThat(numberOfRows("SELECT COUNT(*) FROM EXAMPLE")).isEqualTo(1);
} }
@Test @Test
void whenContinueOnErrorIsFalseThenInitializationFailsOnError() { void whenContinueOnErrorIsFalseThenInitializationFailsOnError() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("data.sql")); settings.setDataLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> initializer.initializeDatabase()); assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> initializer.initializeDatabase());
} }
@Test @Test
void whenContinueOnErrorIsTrueThenInitializationDoesNotFailOnError() { void whenContinueOnErrorIsTrueThenInitializationDoesNotFailOnError() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setContinueOnError(true); settings.setContinueOnError(true);
settings.setDataLocations(Arrays.asList("data.sql")); settings.setDataLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue(); assertThat(initializer.initializeDatabase()).isTrue();
} }
@Test @Test
void whenNoScriptsExistAtASchemaLocationThenInitializationFails() { void whenNoScriptsExistAtASchemaLocationThenInitializationFails() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("does-not-exist.sql")); settings.setSchemaLocations(Arrays.asList("does-not-exist.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase) assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase)
.withMessage("No schema scripts found at location 'does-not-exist.sql'"); .withMessage("No schema scripts found at location 'does-not-exist.sql'");
} }
@Test @Test
void whenNoScriptsExistAtADataLocationThenInitializationFails() { void whenNoScriptsExistAtADataLocationThenInitializationFails() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("does-not-exist.sql")); settings.setDataLocations(Arrays.asList("does-not-exist.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase) assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase)
.withMessage("No data scripts found at location 'does-not-exist.sql'"); .withMessage("No data scripts found at location 'does-not-exist.sql'");
} }
@Test @Test
void whenNoScriptsExistAtAnOptionalSchemaLocationThenInitializationSucceeds() { void whenNoScriptsExistAtAnOptionalSchemaLocationThenInitializationSucceeds() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("optional:does-not-exist.sql")); settings.setSchemaLocations(Arrays.asList("optional:does-not-exist.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse(); assertThat(initializer.initializeDatabase()).isFalse();
} }
@Test @Test
void whenNoScriptsExistAtAnOptionalDataLocationThenInitializationSucceeds() { void whenNoScriptsExistAtAnOptionalDataLocationThenInitializationSucceeds() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("optional:does-not-exist.sql")); settings.setDataLocations(Arrays.asList("optional:does-not-exist.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings); AbstractScriptDatabaseInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse(); assertThat(initializer.initializeDatabase()).isFalse();
} }
private ScriptDataSourceInitializer createInitializer(DataSourceInitializationSettings settings) { protected abstract AbstractScriptDatabaseInitializer createInitializer(DatabaseInitializationSettings settings);
return new ScriptDataSourceInitializer(this.dataSource, settings);
}
private int numberOfRows(String sql) { protected abstract int numberOfRows(String sql);
return new JdbcTemplate(this.dataSource).queryForObject(sql, Integer.class);
}
} }