Move script-based DataSource initializer into spring-boot

Closes gh-25487
Closes gh-25756
pull/25758/head
Andy Wilkinson 4 years ago
parent fa336bb565
commit 5dee68c925

@ -54,7 +54,9 @@ import org.springframework.util.StringUtils;
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,
DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)

@ -1,61 +0,0 @@
/*
* 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.jdbc;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* DML scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DataSourceInitialization implements InitializingBean, ResourceLoaderAware {
private final DataSource dataSource;
private final DataSourceProperties properies;
private volatile ResourceLoader resourceLoader;
/**
* Creates a new {@link DataSourceInitialization} that will initialize the given
* {@code DataSource} using the settings from the given {@code properties}.
* @param dataSource the DataSource to initialize
* @param properies the properties containing the initialization settings
*/
public DataSourceInitialization(DataSource dataSource, DataSourceProperties properies) {
this.dataSource = dataSource;
this.properies = properies;
}
@Override
public void afterPropertiesSet() throws Exception {
new DataSourceInitializer(this.dataSource, this.properies, this.resourceLoader).initializeDataSource();
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}

@ -16,27 +16,158 @@
package org.springframework.boot.autoconfigure.jdbc;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import javax.sql.DataSource;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
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.DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.DifferentCredentialsCondition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.init.DataSourceInitializationSettings;
import org.springframework.boot.jdbc.init.ScriptDataSourceInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.StringUtils;
/**
* Configuration for {@link DataSource} initialization using DDL and DML scripts.
* Configuration for {@link DataSource} initialization using a
* {@link ScriptDataSourceInitializer} with DDL and DML scripts.
*
* @author Andy Wilkinson
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(DataSource.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
class DataSourceInitializationConfiguration {
@Bean
DataSourceInitialization dataSourceInitialization(DataSource dataSource, DataSourceProperties properties) {
return new DataSourceInitialization(dataSource, properties);
private static DataSource determineDataSource(Supplier<DataSource> dataSource, String username, String password,
DataSourceProperties properties) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
DataSourceBuilder.derivedFrom(dataSource.get()).type(SimpleDriverDataSource.class).username(username)
.password(password).build();
}
return dataSource.get();
}
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;
}
// Fully-qualified to work around javac bug in JDK 1.8
@org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
@org.springframework.context.annotation.Conditional(DifferentCredentialsCondition.class)
@org.springframework.context.annotation.Import(DataSourceInitializationDependencyConfigurer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(ScriptDataSourceInitializer.class)
static class InitializationSpecificCredentialsDataSourceInitializationConfiguration {
@Bean
ScriptDataSourceInitializer ddlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource,
DataSourceProperties properties, ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setDdlScriptLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getSqlScriptEncoding());
DataSource initializationDataSource = determineDataSource(dataSource::getObject,
properties.getSchemaUsername(), properties.getSchemaPassword(), properties);
return new InitializationModeDataSourceScriptDatabaseInitializer(initializationDataSource, settings,
properties.getInitializationMode());
}
@Bean
@DependsOn("ddlOnlyScriptDataSourceInitializer")
ScriptDataSourceInitializer dmlOnlyScriptDataSourceInitializer(ObjectProvider<DataSource> dataSource,
DataSourceProperties properties, ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setDmlScriptLocations(scriptLocations(properties.getData(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getSqlScriptEncoding());
DataSource initializationDataSource = determineDataSource(dataSource::getObject,
properties.getDataUsername(), properties.getDataPassword(), properties);
return new InitializationModeDataSourceScriptDatabaseInitializer(initializationDataSource, settings,
properties.getInitializationMode());
}
static class DifferentCredentialsCondition extends AnyNestedCondition {
DifferentCredentialsCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnProperty(prefix = "spring.datasource", name = "schema-username")
static class SchemaCredentials {
}
@ConditionalOnProperty(prefix = "spring.datasource", name = "data-username")
static class DataCredentials {
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(ScriptDataSourceInitializer.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
static class SharedCredentialsDataSourceInitializationConfiguration {
@Bean
ScriptDataSourceInitializer scriptDataSourceInitializer(DataSource dataSource, DataSourceProperties properties,
ResourceLoader resourceLoader) {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setDdlScriptLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform()));
settings.setDmlScriptLocations(scriptLocations(properties.getData(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getSqlScriptEncoding());
return new InitializationModeDataSourceScriptDatabaseInitializer(dataSource, settings,
properties.getInitializationMode());
}
}
static class InitializationModeDataSourceScriptDatabaseInitializer extends ScriptDataSourceInitializer {
private final DataSourceInitializationMode mode;
InitializationModeDataSourceScriptDatabaseInitializer(DataSource dataSource,
DataSourceInitializationSettings settings, DataSourceInitializationMode mode) {
super(dataSource, settings);
this.mode = mode;
}
@Override
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator,
Charset encoding) {
if (this.mode == DataSourceInitializationMode.ALWAYS || (this.mode == DataSourceInitializationMode.EMBEDDED
&& EmbeddedDatabaseConnection.isEmbedded(getDataSource()))) {
super.runScripts(resources, continueOnError, separator, encoding);
}
}
}
}

@ -1,196 +0,0 @@
/*
* 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.jdbc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.config.SortedResourcesFactoryBean;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.util.StringUtils;
/**
* Initialize a {@link DataSource} based on a matching {@link DataSourceProperties}
* config.
*
* @author Dave Syer
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @since 2.5.0
*/
public class DataSourceInitializer {
private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);
private final DataSource dataSource;
private final DataSourceProperties properties;
private final ResourceLoader resourceLoader;
/**
* Create a new instance with the {@link DataSource} to initialize and its matching
* {@link DataSourceProperties configuration}.
* @param dataSource the datasource to initialize
* @param properties the matching configuration
* @param resourceLoader the resource loader to use (can be null)
*/
public DataSourceInitializer(DataSource dataSource, DataSourceProperties properties,
ResourceLoader resourceLoader) {
this.dataSource = dataSource;
this.properties = properties;
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
}
/**
* Initializes the {@link DataSource} by running DDL and DML scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
public boolean initializeDataSource() {
boolean initialized = createSchema();
initialized = initSchema() && initialized;
return initialized;
}
private boolean createSchema() {
List<Resource> scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
if (!isEnabled()) {
logger.debug("Initialization disabled (not running DDL scripts)");
return false;
}
String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword();
runScripts(scripts, username, password);
}
return !scripts.isEmpty();
}
private boolean initSchema() {
List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
if (!scripts.isEmpty()) {
if (!isEnabled()) {
logger.debug("Initialization disabled (not running data scripts)");
return false;
}
String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
runScripts(scripts, username, password);
}
return !scripts.isEmpty();
}
private boolean isEnabled() {
DataSourceInitializationMode mode = this.properties.getInitializationMode();
if (mode == DataSourceInitializationMode.NEVER) {
return false;
}
if (mode == DataSourceInitializationMode.EMBEDDED && !isEmbedded()) {
return false;
}
return true;
}
private boolean isEmbedded() {
try {
return EmbeddedDatabaseConnection.isEmbedded(this.dataSource);
}
catch (Exception ex) {
logger.debug("Could not determine if datasource is embedded", ex);
return false;
}
}
private List<Resource> getScripts(String propertyName, List<String> resources, String fallback) {
if (resources != null) {
return getResources(propertyName, resources, true);
}
String platform = this.properties.getPlatform();
List<String> fallbackResources = new ArrayList<>();
fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql");
fallbackResources.add("classpath*:" + fallback + ".sql");
return getResources(propertyName, fallbackResources, false);
}
private List<Resource> getResources(String propertyName, List<String> locations, boolean validate) {
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (Resource resource : doGetResources(location)) {
if (resource.exists()) {
resources.add(resource);
}
else if (validate) {
throw new InvalidConfigurationPropertyValueException(propertyName, resource,
"No resources were found at location '" + location + "'.");
}
}
}
return resources;
}
private Resource[] doGetResources(String location) {
try {
SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(this.resourceLoader,
Collections.singletonList(location));
factory.afterPropertiesSet();
return factory.getObject();
}
catch (Exception ex) {
throw new IllegalStateException("Unable to load resources from " + location, ex);
}
}
private void runScripts(List<Resource> resources, String username, String password) {
if (resources.isEmpty()) {
return;
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setContinueOnError(this.properties.isContinueOnError());
populator.setSeparator(this.properties.getSeparator());
if (this.properties.getSqlScriptEncoding() != null) {
populator.setSqlScriptEncoding(this.properties.getSqlScriptEncoding().name());
}
for (Resource resource : resources) {
populator.addScript(resource);
}
DataSource dataSource = this.dataSource;
if (StringUtils.hasText(username) && dataSource != null) {
dataSource = DataSourceBuilder.derivedFrom(dataSource).type(SimpleDriverDataSource.class).username(username)
.password(password).build();
}
DatabasePopulatorUtils.execute(populator, dataSource);
}
}

@ -169,5 +169,4 @@ org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvid
# DataSource initializer detectors
org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector=\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDataSourceInitializerDetector,\
org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationDataSourceInitializerDetector
org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDataSourceInitializerDetector

@ -242,10 +242,8 @@ class DataSourceInitializationIntegrationTests {
"spring.datasource.schema:classpath:does/not/exist.sql").run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class);
assertThat(context.getStartupFailure()).hasMessageContaining("[does/not/exist.sql]");
assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.schema");
assertThat(context.getStartupFailure()).hasMessageContaining(
"No resources were found at location 'classpath:does/not/exist.sql'.");
assertThat(context.getStartupFailure())
.hasMessageContaining("No DDL scripts found at location 'classpath:does/not/exist.sql'");
});
}
@ -256,10 +254,8 @@ class DataSourceInitializationIntegrationTests {
"spring.datasource.data:classpath:does/not/exist.sql").run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class);
assertThat(context.getStartupFailure()).hasMessageContaining("[does/not/exist.sql]");
assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.data");
assertThat(context.getStartupFailure()).hasMessageContaining(
"No resources were found at location 'classpath:does/not/exist.sql'.");
assertThat(context.getStartupFailure())
.hasMessageContaining("No DML scripts found at location 'classpath:does/not/exist.sql'");
});
}

@ -1,99 +0,0 @@
/*
* 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.jdbc;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.UUID;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DataSourceInitializer}.
*
* @author Stephane Nicoll
*/
class DataSourceInitializerTests {
@Test
void initializeEmbeddedByDefault() {
try (HikariDataSource dataSource = createDataSource()) {
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
initializer.initializeDataSource();
assertNumberOfRows(jdbcTemplate, 1);
}
}
@Test
void initializeWithModeAlways() {
try (HikariDataSource dataSource = createDataSource()) {
DataSourceProperties properties = new DataSourceProperties();
properties.setInitializationMode(DataSourceInitializationMode.ALWAYS);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
initializer.initializeDataSource();
assertNumberOfRows(jdbcTemplate, 1);
}
}
private void assertNumberOfRows(JdbcTemplate jdbcTemplate, int count) {
assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(count);
}
@Test
void initializeWithModeNever() {
try (HikariDataSource dataSource = createDataSource()) {
DataSourceProperties properties = new DataSourceProperties();
properties.setInitializationMode(DataSourceInitializationMode.NEVER);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null);
assertThat(initializer.initializeDataSource()).isFalse();
}
}
@Test
void initializeOnlyEmbeddedByDefault() throws SQLException {
DatabaseMetaData metadata = mock(DatabaseMetaData.class);
given(metadata.getDatabaseProductName()).willReturn("MySQL");
Connection connection = mock(Connection.class);
given(connection.getMetaData()).willReturn(metadata);
DataSource dataSource = mock(DataSource.class);
given(dataSource.getConnection()).willReturn(connection);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null);
assertThat(initializer.initializeDataSource()).isFalse();
verify(dataSource, times(2)).getConnection();
}
private HikariDataSource createDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).url("jdbc:h2:mem:" + UUID.randomUUID()).build();
}
}

@ -104,7 +104,6 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
"spring.datasource.schema:classpath:/ddl.sql").run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).hasMessageContaining("ddl.sql");
assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.schema");
});
}

@ -2056,6 +2056,10 @@ By default, Spring Boot enables the fail-fast feature of the Spring JDBC initial
This means that, if the scripts cause exceptions, the application fails to start.
You can tune that behavior by setting `spring.datasource.continue-on-error`.
To take complete control over the script-based initialization of a `DataSource`, define your own `ScriptDataSourceInitializer` bean.
Doing so will cause the auto-configuration of script-based initialization to back off.
If you have multiple `DataSource`s in your application, you can define multiple `ScriptDataSourceInitializer` beans.
[[howto-initialize-a-database-using-r2dbc]]

@ -0,0 +1,128 @@
/*
* 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;
/**
* Settings for initializing a database using a JDBC {@link DataSource}.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DataSourceInitializationSettings {
private List<String> ddlScriptLocations;
private List<String> dmlScriptLocations;
private boolean continueOnError = false;
private String separator = ";";
private Charset encoding;
/**
* Returns the locations of the DDL (schema) scripts to apply to the database.
* @return the locations of the DDL scripts
*/
public List<String> getDdlScriptLocations() {
return this.ddlScriptLocations;
}
/**
* Sets the locations of DDL (schema) scripts to apply to the database. By default,
* initialization will fail if a location does not exist. To prevent a failure, a
* location can be made optional by prefixing it with {@code optional:}.
* @param ddlScriptLocations locations of the DDL scripts
*/
public void setDdlScriptLocations(List<String> ddlScriptLocations) {
this.ddlScriptLocations = ddlScriptLocations;
}
/**
* Returns the locations of the DML (data) scripts to apply to the database.
* @return the locations of the DML scripts
*/
public List<String> getDmlScriptLocations() {
return this.dmlScriptLocations;
}
/**
* Sets the locations of DML (data) scripts to apply to the database. By default,
* initialization will fail if a location does not exist. To prevent a failure, a
* location can be made optional by prefixing it with {@code optional:}.
* @param dmlScriptLocations locations of the DML scripts
*/
public void setDmlScriptLocations(List<String> dmlScriptLocations) {
this.dmlScriptLocations = dmlScriptLocations;
}
/**
* Returns whether to continue when an error occurs while applying a DDL or DML
* script.
* @return whether to continue on error
*/
public boolean isContinueOnError() {
return this.continueOnError;
}
/**
* Sets whether initialization should continue when an error occurs when applying a
* DDL or DML script.
* @param continueOnError whether to continue when an error occurs.
*/
public void setContinueOnError(boolean continueOnError) {
this.continueOnError = continueOnError;
}
/**
* Returns the statement separator used in the DDL and DML scripts.
* @return the statement separator
*/
public String getSeparator() {
return this.separator;
}
/**
* Sets the statement separator to use when reading the DDL and DML scripts.
* @param separator statement separator used in DDL and DML scripts
*/
public void setSeparator(String separator) {
this.separator = separator;
}
/**
* Returns the encoding to use when reading the DDL and DML scripts.
* @return the script encoding
*/
public Charset getEncoding() {
return this.encoding;
}
/**
* Sets the encoding to use when reading the DDL and DML scripts.
* @param encoding encoding to use when reading the DDL and DML scripts
*/
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
}

@ -0,0 +1,188 @@
/*
* 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.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
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;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* DML scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class ScriptDataSourceInitializer implements ResourceLoaderAware, InitializingBean {
private static final String OPTIONAL_LOCATION_PREFIX = "optional:";
private final DataSource dataSource;
private final DataSourceInitializationSettings settings;
private volatile ResourceLoader resourceLoader;
/**
* Creates a new {@link ScriptDataSourceInitializer} that will initialize the given
* {@code DataSource} using the given settings.
* @param dataSource data source to initialize
* @param settings initialization settings
*/
public ScriptDataSourceInitializer(DataSource dataSource, DataSourceInitializationSettings settings) {
this.dataSource = dataSource;
this.settings = settings;
}
/**
* Returns the {@code DataSource} that will be initialized.
* @return the initialization data source
*/
protected final DataSource getDataSource() {
return this.dataSource;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
initializeDatabase();
}
/**
* Initializes the database by running DDL and DML scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
public boolean initializeDatabase() {
ScriptLocationResolver locationResolver = new ScriptLocationResolver(this.resourceLoader);
boolean initialized = applyDdlScripts(locationResolver);
initialized = applyDmlScripts(locationResolver) || initialized;
return initialized;
}
private boolean applyDdlScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getDdlScriptLocations(), "DDL", locationResolver);
}
private boolean applyDmlScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getDmlScriptLocations(), "DML", locationResolver);
}
private boolean applyScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
List<Resource> scripts = getScripts(locations, type, locationResolver);
if (!scripts.isEmpty()) {
runScripts(scripts);
}
return !scripts.isEmpty();
}
private List<Resource> getScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
if (CollectionUtils.isEmpty(locations)) {
return Collections.emptyList();
}
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
boolean optional = location.startsWith(OPTIONAL_LOCATION_PREFIX);
if (optional) {
location = location.substring(OPTIONAL_LOCATION_PREFIX.length());
}
for (Resource resource : doGetResources(location, locationResolver)) {
if (resource.exists()) {
resources.add(resource);
}
else if (!optional) {
throw new IllegalStateException("No " + type + " scripts found at location '" + location + "'");
}
}
}
return resources;
}
private List<Resource> doGetResources(String location, ScriptLocationResolver locationResolver) {
try {
return locationResolver.resolve(location);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to load resources from " + location, ex);
}
}
private void runScripts(List<Resource> resources) {
if (resources.isEmpty()) {
return;
}
runScripts(resources, this.settings.isContinueOnError(), this.settings.getSeparator(),
this.settings.getEncoding());
}
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);
}
private static class ScriptLocationResolver {
private final ResourcePatternResolver resourcePatternResolver;
ScriptLocationResolver(ResourceLoader resourceLoader) {
this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
}
private List<Resource> resolve(String location) throws IOException {
List<Resource> resources = new ArrayList<>(
Arrays.asList(this.resourcePatternResolver.getResources(location)));
resources.sort((r1, r2) -> {
try {
return r1.getURL().toString().compareTo(r2.getURL().toString());
}
catch (IOException ex) {
return 0;
}
});
return resources;
}
}
}

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jdbc;
package org.springframework.boot.jdbc.init;
import java.util.Collections;
import java.util.Set;
@ -23,15 +23,15 @@ import org.springframework.boot.jdbc.init.dependency.AbstractBeansOfTypeDataSour
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector;
/**
* A {@link DataSourceInitializerDetector} for {@link DataSourceInitialization}.
* A {@link DataSourceInitializerDetector} for {@link ScriptDataSourceInitializer}.
*
* @author Andy Wilkinson
*/
class DataSourceInitializationDataSourceInitializerDetector extends AbstractBeansOfTypeDataSourceInitializerDetector {
class ScriptDataSourceInitializerDetector extends AbstractBeansOfTypeDataSourceInitializerDetector {
@Override
protected Set<Class<?>> getDataSourceInitializerBeanTypes() {
return Collections.singleton(DataSourceInitialization.class);
return Collections.singleton(ScriptDataSourceInitializer.class);
}
}

@ -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 a JDBC {@code DataSource}.
*/
package org.springframework.boot.jdbc.init;

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

@ -0,0 +1,83 @@
/*
* 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.Arrays;
import java.util.UUID;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.assertThatExceptionOfType;
/**
* Tests for {@link ScriptDataSourceInitializer}.
*
* @author Andy Wilkinson
*/
class ScriptDataSourceInitializerTests {
private final HikariDataSource dataSource = DataSourceBuilder.create().type(HikariDataSource.class)
.url("jdbc:h2:mem:" + UUID.randomUUID()).build();
@AfterEach
void closeDataSource() {
this.dataSource.close();
}
@Test
void whenDatabaseIsInitializedThenDdlAndDmlScriptsAreApplied() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setDdlScriptLocations(Arrays.asList("schema.sql"));
settings.setDmlScriptLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfRows("SELECT COUNT(*) FROM EXAMPLE")).isEqualTo(1);
}
@Test
void whenContinueOnErrorIsFalseThenInitializationFailsOnError() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setDmlScriptLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings);
assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> initializer.initializeDatabase());
}
@Test
void whenContinueOnErrorIsTrueThenInitializationDoesNotFailOnError() {
DataSourceInitializationSettings settings = new DataSourceInitializationSettings();
settings.setContinueOnError(true);
settings.setDmlScriptLocations(Arrays.asList("data.sql"));
ScriptDataSourceInitializer initializer = createInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
}
private ScriptDataSourceInitializer createInitializer(DataSourceInitializationSettings settings) {
return new ScriptDataSourceInitializer(this.dataSource, settings);
}
private int numberOfRows(String sql) {
return new JdbcTemplate(this.dataSource).queryForObject(sql, Integer.class);
}
}

@ -0,0 +1 @@
INSERT INTO EXAMPLE VALUES (1, 'Andy');

@ -0,0 +1,4 @@
CREATE TABLE EXAMPLE (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(30)
);
Loading…
Cancel
Save