From 5dee68c925b71273fc9f6ea21512b656370b512f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 22 Mar 2021 18:18:18 +0000 Subject: [PATCH] Move script-based DataSource initializer into spring-boot Closes gh-25487 Closes gh-25756 --- .../jdbc/DataSourceAutoConfiguration.java | 4 +- .../jdbc/DataSourceInitialization.java | 61 ------ ...DataSourceInitializationConfiguration.java | 145 ++++++++++++- .../jdbc/DataSourceInitializer.java | 196 ------------------ .../main/resources/META-INF/spring.factories | 3 +- ...aSourceInitializationIntegrationTests.java | 12 +- .../jdbc/DataSourceInitializerTests.java | 99 --------- .../HibernateJpaAutoConfigurationTests.java | 1 - .../src/docs/asciidoc/howto.adoc | 4 + .../DataSourceInitializationSettings.java | 128 ++++++++++++ .../init/ScriptDataSourceInitializer.java | 188 +++++++++++++++++ .../ScriptDataSourceInitializerDetector.java} | 8 +- .../boot/jdbc/init/package-info.java | 20 ++ .../main/resources/META-INF/spring.factories | 1 + .../ScriptDataSourceInitializerTests.java | 83 ++++++++ .../spring-boot/src/test/resources/data.sql | 1 + .../spring-boot/src/test/resources/schema.sql | 4 + 17 files changed, 579 insertions(+), 379 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/DataSourceInitializationSettings.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializer.java rename spring-boot-project/{spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationDataSourceInitializerDetector.java => spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerDetector.java} (75%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/package-info.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerTests.java create mode 100644 spring-boot-project/spring-boot/src/test/resources/data.sql create mode 100644 spring-boot-project/spring-boot/src/test/resources/schema.sql diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index d3cb0a138e6..e2c58f43ccf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -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) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java deleted file mode 100644 index 6bcd2e91523..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java index 35b26cf81a5..c32975bff5f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java @@ -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, 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 scriptLocations(List locations, String fallback, String platform) { + if (locations != null) { + return locations; + } + List 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, + 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, + 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 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); + } + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java deleted file mode 100644 index d95c11b0fb3..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java +++ /dev/null @@ -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 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 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 getScripts(String propertyName, List resources, String fallback) { - if (resources != null) { - return getResources(propertyName, resources, true); - } - String platform = this.properties.getPlatform(); - List fallbackResources = new ArrayList<>(); - fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql"); - fallbackResources.add("classpath*:" + fallback + ".sql"); - return getResources(propertyName, fallbackResources, false); - } - - private List getResources(String propertyName, List locations, boolean validate) { - List 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 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); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 59430077d6a..730818f509d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -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 \ No newline at end of file diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java index be62afcfd05..427f9804e46 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java @@ -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'"); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java deleted file mode 100644 index 4fd27fa7214..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index c55b3c7e62d..96bd4909ecd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -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"); }); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc index 3fb0fc50997..2605d2adf2c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc @@ -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]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/DataSourceInitializationSettings.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/DataSourceInitializationSettings.java new file mode 100644 index 00000000000..8df5d9058ac --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/DataSourceInitializationSettings.java @@ -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 ddlScriptLocations; + + private List 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 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 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 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 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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializer.java new file mode 100644 index 00000000000..1d9b452a6a4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializer.java @@ -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 locations, String type, ScriptLocationResolver locationResolver) { + List scripts = getScripts(locations, type, locationResolver); + if (!scripts.isEmpty()) { + runScripts(scripts); + } + return !scripts.isEmpty(); + } + + private List getScripts(List locations, String type, ScriptLocationResolver locationResolver) { + if (CollectionUtils.isEmpty(locations)) { + return Collections.emptyList(); + } + List 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 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 resources) { + if (resources.isEmpty()) { + return; + } + runScripts(resources, this.settings.isContinueOnError(), this.settings.getSeparator(), + this.settings.getEncoding()); + } + + protected void runScripts(List 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 resolve(String location) throws IOException { + List 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationDataSourceInitializerDetector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerDetector.java similarity index 75% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationDataSourceInitializerDetector.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerDetector.java index 3b441089082..254d62b25d7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationDataSourceInitializerDetector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerDetector.java @@ -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> getDataSourceInitializerBeanTypes() { - return Collections.singleton(DataSourceInitialization.class); + return Collections.singleton(ScriptDataSourceInitializer.class); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/package-info.java new file mode 100644 index 00000000000..4495a2b635b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index dbfe0c429dc..d0812eefc93 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerTests.java new file mode 100644 index 00000000000..1e7675fa960 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/resources/data.sql b/spring-boot-project/spring-boot/src/test/resources/data.sql new file mode 100644 index 00000000000..06acad8464b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/data.sql @@ -0,0 +1 @@ +INSERT INTO EXAMPLE VALUES (1, 'Andy'); \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/resources/schema.sql b/spring-boot-project/spring-boot/src/test/resources/schema.sql new file mode 100644 index 00000000000..73d14cd310a --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE EXAMPLE ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(30) +); \ No newline at end of file