Create spring-boot-sql module

This commit is contained in:
Andy Wilkinson
2025-04-04 15:20:14 +01:00
committed by Phillip Webb
parent e288c81b7b
commit 837cf467e0
47 changed files with 76 additions and 37 deletions

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 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.sql.autoconfigure.init;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.ImportRuntimeHints;
/**
* Marker interface for a script-based database initializer that initializes the
* application's database.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 4.0.0
* @see AbstractScriptDatabaseInitializer
*/
@ImportRuntimeHints(SqlInitializationScriptsRuntimeHints.class)
public interface ApplicationScriptDatabaseInitializer {
/**
* Adapts {@link SqlInitializationProperties} to
* {@link DatabaseInitializationSettings}.
* @param properties the properties to adapt
* @return the settings
*/
static DatabaseInitializationSettings getSettings(SqlInitializationProperties properties) {
return SettingsCreator.createFrom(properties);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2025 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.sql.autoconfigure.init;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when {@code spring.sql.init.mode} is set to a value other than
* {@link DatabaseInitializationMode#NEVER}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 4.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Conditional(OnSqlInitializationCondition.class)
public @interface ConditionalOnSqlInitialization {
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2025 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.sql.autoconfigure.init;
import java.util.Locale;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;
/**
* Condition that checks if the database initialization of a particular component should
* be considered.
*
* @author Stephane Nicoll
* @since 2.6.2
* @see DatabaseInitializationMode
*/
public abstract class OnDatabaseInitializationCondition extends SpringBootCondition {
private final String name;
private final String[] propertyNames;
/**
* Create a new instance with the name of the component and the property names to
* check, in order. If a property is set, its value is used to determine the outcome
* and remaining properties are not tested.
* @param name the name of the component
* @param propertyNames the properties to check (in order)
*/
protected OnDatabaseInitializationCondition(String name, String... propertyNames) {
this.name = name;
this.propertyNames = propertyNames;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String propertyName = getConfiguredProperty(environment);
DatabaseInitializationMode mode = getDatabaseInitializationMode(environment, propertyName);
boolean match = match(mode);
String messagePrefix = (propertyName != null) ? propertyName : "default value";
return new ConditionOutcome(match, ConditionMessage.forCondition(this.name + "Database Initialization")
.because(messagePrefix + " is " + mode));
}
private boolean match(DatabaseInitializationMode mode) {
return !mode.equals(DatabaseInitializationMode.NEVER);
}
private DatabaseInitializationMode getDatabaseInitializationMode(Environment environment, String propertyName) {
if (StringUtils.hasText(propertyName)) {
String candidate = environment.getProperty(propertyName, "embedded").toUpperCase(Locale.ENGLISH);
if (StringUtils.hasText(candidate)) {
return DatabaseInitializationMode.valueOf(candidate);
}
}
return DatabaseInitializationMode.EMBEDDED;
}
private String getConfiguredProperty(Environment environment) {
for (String propertyName : this.propertyNames) {
if (environment.containsProperty(propertyName)) {
return propertyName;
}
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2012-2025 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.sql.autoconfigure.init;
import org.springframework.context.annotation.Condition;
/**
* {@link Condition} implementation for {@link ConditionalOnSqlInitialization}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class OnSqlInitializationCondition extends OnDatabaseInitializationCondition {
OnSqlInitializationCondition() {
super("SQL Initialization", "spring.sql.init.mode");
}
}

View File

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

View File

@@ -0,0 +1,155 @@
/*
* 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.sql.autoconfigure.init;
import java.nio.charset.Charset;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
/**
* {@link ConfigurationProperties Configuration properties} for initializing an SQL
* database.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
@ConfigurationProperties("spring.sql.init")
public class SqlInitializationProperties {
/**
* Locations of the schema (DDL) scripts to apply to the database.
*/
private List<String> schemaLocations;
/**
* Locations of the data (DML) scripts to apply to the database.
*/
private List<String> dataLocations;
/**
* Platform to use in the default schema or data script locations,
* schema-${platform}.sql and data-${platform}.sql.
*/
private String platform = "all";
/**
* Username of the database to use when applying initialization scripts (if
* different).
*/
private String username;
/**
* Password of the database to use when applying initialization scripts (if
* different).
*/
private String password;
/**
* Whether initialization should continue when an error occurs.
*/
private boolean continueOnError = false;
/**
* Statement separator in the schema and data scripts.
*/
private String separator = ";";
/**
* Encoding of the schema and data scripts.
*/
private Charset encoding;
/**
* Mode to apply when determining whether initialization should be performed.
*/
private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED;
public List<String> getSchemaLocations() {
return this.schemaLocations;
}
public void setSchemaLocations(List<String> schemaLocations) {
this.schemaLocations = schemaLocations;
}
public List<String> getDataLocations() {
return this.dataLocations;
}
public void setDataLocations(List<String> dataLocations) {
this.dataLocations = dataLocations;
}
public String getPlatform() {
return this.platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isContinueOnError() {
return this.continueOnError;
}
public void setContinueOnError(boolean continueOnError) {
this.continueOnError = continueOnError;
}
public String getSeparator() {
return this.separator;
}
public void setSeparator(String separator) {
this.separator = separator;
}
public Charset getEncoding() {
return this.encoding;
}
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
public DatabaseInitializationMode getMode() {
return this.mode;
}
public void setMode(DatabaseInitializationMode mode) {
this.mode = mode;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2022 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.sql.autoconfigure.init;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
/**
* {@link RuntimeHintsRegistrar} for SQL initialization scripts.
*
* @author Moritz Halbritter
*/
class SqlInitializationScriptsRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("schema.sql").registerPattern("schema-*.sql");
hints.resources().registerPattern("data.sql").registerPattern("data-*.sql");
}
}

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.
*/
/**
* Auto-configuration for basic script-based initialization of an SQL database.
*/
package org.springframework.boot.sql.autoconfigure.init;

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2012-2025 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.sql.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.Iterator;
import java.util.List;
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.util.CollectionUtils;
/**
* Base class for an {@link InitializingBean} that performs SQL database initialization
* using schema (DDL) and data (DML) scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
private static final String OPTIONAL_LOCATION_PREFIX = "optional:";
private final DatabaseInitializationSettings settings;
private volatile ResourceLoader resourceLoader;
/**
* Creates a new {@link AbstractScriptDatabaseInitializer} that will initialize the
* database using the given settings.
* @param settings initialization settings
*/
protected AbstractScriptDatabaseInitializer(DatabaseInitializationSettings settings) {
this.settings = settings;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
initializeDatabase();
}
/**
* Initializes the database by applying schema and data 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 = applySchemaScripts(locationResolver);
return applyDataScripts(locationResolver) || initialized;
}
private boolean isEnabled() {
if (this.settings.getMode() == DatabaseInitializationMode.NEVER) {
return false;
}
return this.settings.getMode() == DatabaseInitializationMode.ALWAYS || isEmbeddedDatabase();
}
/**
* Returns whether the database that is to be initialized is embedded.
* @return {@code true} if the database is embedded, otherwise {@code false}
* @since 2.5.1
*/
protected boolean isEmbeddedDatabase() {
throw new IllegalStateException(
"Database initialization mode is '" + this.settings.getMode() + "' and database type is unknown");
}
private boolean applySchemaScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getSchemaLocations(), "schema", locationResolver);
}
private boolean applyDataScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getDataLocations(), "data", locationResolver);
}
private boolean applyScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
List<Resource> scripts = getScripts(locations, type, locationResolver);
if (!scripts.isEmpty() && isEnabled()) {
runScripts(scripts);
return true;
}
return false;
}
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.isReadable()) {
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) {
runScripts(new Scripts(resources).continueOnError(this.settings.isContinueOnError())
.separator(this.settings.getSeparator())
.encoding(this.settings.getEncoding()));
}
/**
* Initialize the database by running the given {@code scripts}.
* @param scripts the scripts to run
* @since 3.0.0
*/
protected abstract void runScripts(Scripts scripts);
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;
}
}
/**
* Scripts to be used to initialize the database.
*
* @since 3.0.0
*/
public static class Scripts implements Iterable<Resource> {
private final List<Resource> resources;
private boolean continueOnError;
private String separator = ";";
private Charset encoding;
public Scripts(List<Resource> resources) {
this.resources = resources;
}
@Override
public Iterator<Resource> iterator() {
return this.resources.iterator();
}
public Scripts continueOnError(boolean continueOnError) {
this.continueOnError = continueOnError;
return this;
}
public boolean isContinueOnError() {
return this.continueOnError;
}
public Scripts separator(String separator) {
this.separator = separator;
return this;
}
public String getSeparator() {
return this.separator;
}
public Scripts encoding(Charset encoding) {
this.encoding = encoding;
return this;
}
public Charset getEncoding() {
return this.encoding;
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.sql.init;
/**
* Supported database initialization modes.
*
* @author Andy Wilkinson
* @since 2.5.1
* @see AbstractScriptDatabaseInitializer
*/
public enum DatabaseInitializationMode {
/**
* Always initialize the database.
*/
ALWAYS,
/**
* Only initialize an embedded database.
*/
EMBEDDED,
/**
* Never initialize the database.
*/
NEVER
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2012-2025 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.sql.init;
import java.nio.charset.Charset;
import java.util.List;
/**
* Settings for initializing an SQL database.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DatabaseInitializationSettings {
private List<String> schemaLocations;
private List<String> dataLocations;
private boolean continueOnError;
private String separator = ";";
private Charset encoding;
private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED;
/**
* Returns the locations of the schema (DDL) scripts to apply to the database.
* @return the locations of the schema scripts
*/
public List<String> getSchemaLocations() {
return this.schemaLocations;
}
/**
* Sets the locations of schema (DDL) 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 schemaLocations locations of the schema scripts
*/
public void setSchemaLocations(List<String> schemaLocations) {
this.schemaLocations = schemaLocations;
}
/**
* Returns the locations of data (DML) scripts to apply to the database.
* @return the locations of the data scripts
*/
public List<String> getDataLocations() {
return this.dataLocations;
}
/**
* Sets the locations of data (DML) 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 dataLocations locations of the data scripts
*/
public void setDataLocations(List<String> dataLocations) {
this.dataLocations = dataLocations;
}
/**
* Returns whether to continue when an error occurs while applying a schema or data
* 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
* schema or data 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 schema and data scripts.
* @return the statement separator
*/
public String getSeparator() {
return this.separator;
}
/**
* Sets the statement separator to use when reading the schema and data scripts.
* @param separator statement separator used in the schema and data scripts
*/
public void setSeparator(String separator) {
this.separator = separator;
}
/**
* Returns the encoding to use when reading the schema and data scripts.
* @return the script encoding
*/
public Charset getEncoding() {
return this.encoding;
}
/**
* Sets the encoding to use when reading the schema and data scripts.
* @param encoding encoding to use when reading the schema and data scripts
*/
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
/**
* Gets the mode to use when determining whether database initialization should be
* performed.
* @return the initialization mode
* @since 2.5.1
*/
public DatabaseInitializationMode getMode() {
return this.mode;
}
/**
* Sets the mode the use when determining whether database initialization should be
* performed.
* @param mode the initialization mode
* @since 2.5.1
*/
public void setMode(DatabaseInitializationMode mode) {
this.mode = mode;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.sql.init.dependency;
import java.util.Collections;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/**
* Base class for {@link DatabaseInitializerDetector DatabaseInitializerDetectors} that
* detect database initializer beans by type.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public abstract class AbstractBeansOfTypeDatabaseInitializerDetector implements DatabaseInitializerDetector {
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
try {
Set<Class<?>> types = getDatabaseInitializerBeanTypes();
return new BeansOfTypeDetector(types).detect(beanFactory);
}
catch (Throwable ex) {
return Collections.emptySet();
}
}
/**
* Returns the bean types that should be detected as being database initializers.
* @return the database initializer bean types
*/
protected abstract Set<Class<?>> getDatabaseInitializerBeanTypes();
}

View File

@@ -0,0 +1,53 @@
/*
* 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.sql.init.dependency;
import java.util.Collections;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/**
* Base class for {@link DependsOnDatabaseInitializationDetector
* DependsOnDatabaseInitializationDetectors} that detect by type beans that depend upon
* database initialization.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public abstract class AbstractBeansOfTypeDependsOnDatabaseInitializationDetector
implements DependsOnDatabaseInitializationDetector {
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
try {
Set<Class<?>> types = getDependsOnDatabaseInitializationBeanTypes();
return new BeansOfTypeDetector(types).detect(beanFactory);
}
catch (Throwable ex) {
return Collections.emptySet();
}
}
/**
* Returns the bean types that should be detected as depending on database
* initialization.
* @return the database initialization dependent bean types
*/
protected abstract Set<Class<?>> getDependsOnDatabaseInitializationBeanTypes();
}

View File

@@ -0,0 +1,43 @@
/*
* 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.sql.init.dependency;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/**
* {@link DependsOnDatabaseInitializationDetector} that detects beans annotated with
* {@link DependsOnDatabaseInitialization}.
*
* @author Andy Wilkinson
*/
class AnnotationDependsOnDatabaseInitializationDetector implements DependsOnDatabaseInitializationDetector {
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
Set<String> dependentBeans = new HashSet<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
if (beanFactory.findAnnotationOnBean(beanName, DependsOnDatabaseInitialization.class, false) != null) {
dependentBeans.add(beanName);
}
}
return dependentBeans;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.sql.init.dependency;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
/**
* Helper class for detecting beans of particular types in a bean factory.
*
* @author Andy Wilkinson
*/
class BeansOfTypeDetector {
private final Set<Class<?>> types;
BeansOfTypeDetector(Set<Class<?>> types) {
this.types = types;
}
Set<String> detect(ListableBeanFactory beanFactory) {
Set<String> beanNames = new HashSet<>();
for (Class<?> type : this.types) {
try {
String[] names = beanFactory.getBeanNamesForType(type, true, false);
Arrays.stream(names).map(BeanFactoryUtils::transformedBeanName).forEach(beanNames::add);
}
catch (Throwable ex) {
// Continue
}
}
return beanNames;
}
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright 2012-2023 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.sql.init.dependency;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.aot.AotDetector;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Configures beans that depend upon SQL database initialization with
* {@link BeanDefinition#getDependsOn() dependencies} upon beans that perform database
* initialization. Intended for {@link Import import} in configuration classes that define
* database initialization beans or that define beans that require database initialization
* to have completed before they are initialized.
* <p>
* Beans that initialize a database are identified by {@link DatabaseInitializerDetector
* DatabaseInitializerDetectors}. Beans that depend upon database initialization are
* identified by {@link DependsOnDatabaseInitializationDetector
* DependsOnDatabaseInitializationDetectors}.
*
* @author Andy Wilkinson
* @since 2.5.0
* @see DatabaseInitializerDetector
* @see DependsOnDatabaseInitializationDetector
* @see DependsOnDatabaseInitialization
*/
public class DatabaseInitializationDependencyConfigurer implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
String name = DependsOnDatabaseInitializationPostProcessor.class.getName();
if (!registry.containsBeanDefinition(name)) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(DependsOnDatabaseInitializationPostProcessor.class);
registry.registerBeanDefinition(name, builder.getBeanDefinition());
}
}
/**
* {@link BeanFactoryPostProcessor} used to configure database initialization
* dependency relationships.
*/
static class DependsOnDatabaseInitializationPostProcessor
implements BeanFactoryPostProcessor, EnvironmentAware, Ordered {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
InitializerBeanNames initializerBeanNames = detectInitializerBeanNames(beanFactory);
if (initializerBeanNames.isEmpty()) {
return;
}
Set<String> previousInitializerBeanNamesBatch = null;
for (Set<String> initializerBeanNamesBatch : initializerBeanNames.batchedBeanNames()) {
for (String initializerBeanName : initializerBeanNamesBatch) {
BeanDefinition beanDefinition = getBeanDefinition(initializerBeanName, beanFactory);
beanDefinition
.setDependsOn(merge(beanDefinition.getDependsOn(), previousInitializerBeanNamesBatch));
}
previousInitializerBeanNamesBatch = initializerBeanNamesBatch;
}
for (String dependsOnInitializationBeanNames : detectDependsOnInitializationBeanNames(beanFactory)) {
BeanDefinition beanDefinition = getBeanDefinition(dependsOnInitializationBeanNames, beanFactory);
beanDefinition.setDependsOn(merge(beanDefinition.getDependsOn(), initializerBeanNames.beanNames()));
}
}
private String[] merge(String[] source, Set<String> additional) {
if (CollectionUtils.isEmpty(additional)) {
return source;
}
Set<String> result = new LinkedHashSet<>((source != null) ? Arrays.asList(source) : Collections.emptySet());
result.addAll(additional);
return StringUtils.toStringArray(result);
}
private InitializerBeanNames detectInitializerBeanNames(ConfigurableListableBeanFactory beanFactory) {
List<DatabaseInitializerDetector> detectors = getDetectors(beanFactory, DatabaseInitializerDetector.class);
InitializerBeanNames initializerBeanNames = new InitializerBeanNames();
for (DatabaseInitializerDetector detector : detectors) {
for (String beanName : detector.detect(beanFactory)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
beanDefinition.setAttribute(DatabaseInitializerDetector.class.getName(),
detector.getClass().getName());
initializerBeanNames.detected(detector, beanName);
}
}
for (DatabaseInitializerDetector detector : detectors) {
detector.detectionComplete(beanFactory, initializerBeanNames.beanNames());
}
return initializerBeanNames;
}
private Collection<String> detectDependsOnInitializationBeanNames(ConfigurableListableBeanFactory beanFactory) {
List<DependsOnDatabaseInitializationDetector> detectors = getDetectors(beanFactory,
DependsOnDatabaseInitializationDetector.class);
Set<String> beanNames = new HashSet<>();
for (DependsOnDatabaseInitializationDetector detector : detectors) {
beanNames.addAll(detector.detect(beanFactory));
}
return beanNames;
}
private <T> List<T> getDetectors(ConfigurableListableBeanFactory beanFactory, Class<T> type) {
ArgumentResolver argumentResolver = ArgumentResolver.of(Environment.class, this.environment);
return SpringFactoriesLoader.forDefaultResourceLocation(beanFactory.getBeanClassLoader())
.load(type, argumentResolver);
}
private static BeanDefinition getBeanDefinition(String beanName, ConfigurableListableBeanFactory beanFactory) {
try {
return beanFactory.getBeanDefinition(beanName);
}
catch (NoSuchBeanDefinitionException ex) {
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
if (parentBeanFactory instanceof ConfigurableListableBeanFactory configurableBeanFactory) {
return getBeanDefinition(beanName, configurableBeanFactory);
}
throw ex;
}
}
static class InitializerBeanNames {
private final Map<DatabaseInitializerDetector, Set<String>> byDetectorBeanNames = new LinkedHashMap<>();
private final Set<String> beanNames = new LinkedHashSet<>();
private void detected(DatabaseInitializerDetector detector, String beanName) {
this.byDetectorBeanNames.computeIfAbsent(detector, (key) -> new LinkedHashSet<>()).add(beanName);
this.beanNames.add(beanName);
}
private boolean isEmpty() {
return this.beanNames.isEmpty();
}
private Iterable<Set<String>> batchedBeanNames() {
return this.byDetectorBeanNames.values();
}
private Set<String> beanNames() {
return Collections.unmodifiableSet(this.beanNames);
}
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.sql.init.dependency;
import java.util.Set;
import javax.sql.DataSource;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.Ordered;
/**
* Detects beans that initialize an SQL database. Implementations should be registered in
* {@code META-INF/spring.factories} under the key
* {@code org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector}.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public interface DatabaseInitializerDetector extends Ordered {
/**
* Detect beans defined in the given {@code beanFactory} that initialize a
* {@link DataSource}.
* @param beanFactory bean factory to examine
* @return names of the detected {@code DataSource} initializer beans, or an empty set
* if none were detected.
*/
Set<String> detect(ConfigurableListableBeanFactory beanFactory);
/**
* Callback indicating that all known {@code DataSourceInitializerDetectors} have been
* called and detection of beans that initialize a {@link DataSource} is complete.
* @param beanFactory bean factory that was examined
* @param dataSourceInitializerNames names of the {@code DataSource} initializer beans
* detected by all known detectors
*/
default void detectionComplete(ConfigurableListableBeanFactory beanFactory,
Set<String> dataSourceInitializerNames) {
}
@Override
default int getOrder() {
return 0;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.sql.init.dependency;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Bean;
/**
* Indicate that a bean's creation and initialization depends upon database initialization
* having completed. May be used on a bean's class or its {@link Bean @Bean} definition.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DependsOnDatabaseInitialization {
}

View File

@@ -0,0 +1,41 @@
/*
* 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.sql.init.dependency;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/**
* Detects beans that depend on database initialization. Implementations should be
* registered in {@code META-INF/spring.factories} under the key
* {@code org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector}.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public interface DependsOnDatabaseInitializationDetector {
/**
* Detect beans defined in the given {@code beanFactory} that depend on database
* initialization. If no beans are detected, an empty set is returned.
* @param beanFactory bean factory to examine
* @return names of any beans that depend upon database initialization
*/
Set<String> detect(ConfigurableListableBeanFactory beanFactory);
}

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.
*/
/**
* Infrastructure for establishing database initialization bean dependencies.
*/
package org.springframework.boot.sql.init.dependency;

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 initialization of an SQL database.
*/
package org.springframework.boot.sql.init;

View File

@@ -0,0 +1,14 @@
{
"properties": [
{
"name": "spring.sql.init.enabled",
"type": "java.lang.Boolean",
"description": "Whether basic script-based initialization of an SQL database is enabled.",
"defaultValue": true,
"deprecation": {
"replacement": "spring.sql.init.mode",
"level": "warning"
}
}
]
}

View File

@@ -0,0 +1,3 @@
# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2025 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.sql.autoconfigure.init;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OnDatabaseInitializationCondition}.
*
* @author Stephane Nicoll
*/
class OnDatabaseInitializationConditionTests {
@Test
void getMatchOutcomeWithPropertyNoSetMatches() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.another", "noise")), null);
assertThat(outcome.isMatch()).isTrue();
}
@Test
void getMatchOutcomeWithPropertySetToAlwaysMatches() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-mode=always")), null);
assertThat(outcome.isMatch()).isTrue();
}
@Test
void getMatchOutcomeWithPropertySetToEmbeddedMatches() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-mode=embedded")), null);
assertThat(outcome.isMatch()).isTrue();
}
@Test
void getMatchOutcomeWithPropertySetToNeverDoesNotMatch() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-mode=never")), null);
assertThat(outcome.isMatch()).isFalse();
}
@Test
void getMatchOutcomeWithPropertySetToEmptyStringIsIgnored() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-mode")), null);
assertThat(outcome.isMatch()).isTrue();
}
@Test
void getMatchOutcomeWithMultiplePropertiesUsesFirstSet() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode",
"test.schema-mode", "test.init-schema-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-schema-mode=embedded")), null);
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage()).isEqualTo("TestDatabase Initialization test.init-schema-mode is EMBEDDED");
}
@Test
void getMatchOutcomeHasDedicatedDescription() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(TestPropertyValues.of("test.init-mode=embedded")), null);
assertThat(outcome.getMessage()).isEqualTo("TestDatabase Initialization test.init-mode is EMBEDDED");
}
@Test
void getMatchOutcomeHasWhenPropertyIsNotSetHasDefaultDescription() {
OnDatabaseInitializationCondition condition = new OnTestDatabaseInitializationCondition("test.init-mode");
ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(TestPropertyValues.empty()), null);
assertThat(outcome.getMessage()).isEqualTo("TestDatabase Initialization default value is EMBEDDED");
}
private ConditionContext mockConditionContext(TestPropertyValues propertyValues) {
MockEnvironment environment = new MockEnvironment();
propertyValues.applyTo(environment);
ConditionContext conditionContext = mock(ConditionContext.class);
given(conditionContext.getEnvironment()).willReturn(environment);
return conditionContext;
}
static class OnTestDatabaseInitializationCondition extends OnDatabaseInitializationCondition {
OnTestDatabaseInitializationCondition(String... properties) {
super("Test", properties);
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2022 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.sql.autoconfigure.init;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SqlInitializationScriptsRuntimeHints}.
*
* @author Moritz Halbritter
*/
class SqlInitializationScriptsRuntimeHintsTests {
@Test
void shouldRegisterSchemaHints() {
RuntimeHints hints = new RuntimeHints();
new SqlInitializationScriptsRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("schema.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("schema-all.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("schema-mysql.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("schema-postgres.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("schema-oracle.sql")).accepts(hints);
}
@Test
void shouldRegisterDataHints() {
RuntimeHints hints = new RuntimeHints();
new SqlInitializationScriptsRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("data.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("data-all.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("data-mysql.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("data-postgres.sql")).accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forResource("data-oracle.sql")).accepts(hints);
}
}

View File

@@ -0,0 +1,379 @@
/*
* Copyright 2012-2023 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.sql.init.dependency;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
/**
* Tests for {@link DatabaseInitializationDependencyConfigurer}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class DatabaseInitializationDependencyConfigurerTests {
private final ConfigurableEnvironment environment = new MockEnvironment();
@TempDir
File temp;
@BeforeEach
void resetMocks() {
reset(MockDatabaseInitializerDetector.instance, OrderedNearLowestMockDatabaseInitializerDetector.instance,
OrderedLowestMockDatabaseInitializerDetector.instance,
MockedDependsOnDatabaseInitializationDetector.instance);
}
@Test
void beanFactoryPostProcessorHasOrderAllowingSubsequentPostProcessorsToFineTuneDependencies() {
performDetection(Arrays.asList(MockDatabaseInitializerDetector.class,
MockedDependsOnDatabaseInitializationDetector.class), (context) -> {
BeanDefinition alpha = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition bravo = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
context.register(DependsOnCaptor.class);
context.register(DependencyConfigurerConfiguration.class);
context.registerBeanDefinition("alpha", alpha);
context.registerBeanDefinition("bravo", bravo);
given(MockDatabaseInitializerDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("alpha"));
given(MockedDependsOnDatabaseInitializationDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("bravo"));
context.refresh();
assertThat(DependsOnCaptor.dependsOn).hasEntrySatisfying("bravo",
(dependencies) -> assertThat(dependencies).containsExactly("alpha"));
assertThat(DependsOnCaptor.dependsOn).doesNotContainKey("alpha");
});
}
@Test
void whenDetectorsAreCreatedThenTheEnvironmentCanBeInjected() {
performDetection(Arrays.asList(ConstructorInjectionDatabaseInitializerDetector.class,
ConstructorInjectionDependsOnDatabaseInitializationDetector.class), (context) -> {
BeanDefinition alpha = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
context.registerBeanDefinition("alpha", alpha);
context.register(DependencyConfigurerConfiguration.class);
context.refresh();
assertThat(ConstructorInjectionDatabaseInitializerDetector.environment).isEqualTo(this.environment);
assertThat(ConstructorInjectionDependsOnDatabaseInitializationDetector.environment)
.isEqualTo(this.environment);
});
}
@Test
void whenDependenciesAreConfiguredThenBeansThatDependUponDatabaseInitializationDependUponDetectedDatabaseInitializers() {
BeanDefinition alpha = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition bravo = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
performDetection(Arrays.asList(MockDatabaseInitializerDetector.class,
MockedDependsOnDatabaseInitializationDetector.class), (context) -> {
context.registerBeanDefinition("alpha", alpha);
context.registerBeanDefinition("bravo", bravo);
given(MockDatabaseInitializerDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("alpha"));
given(MockedDependsOnDatabaseInitializationDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("bravo"));
context.register(DependencyConfigurerConfiguration.class);
context.refresh();
assertThat(alpha.getAttribute(DatabaseInitializerDetector.class.getName()))
.isEqualTo(MockDatabaseInitializerDetector.class.getName());
assertThat(bravo.getAttribute(DatabaseInitializerDetector.class.getName())).isNull();
then(MockDatabaseInitializerDetector.instance).should()
.detectionComplete(context.getBeanFactory(), Collections.singleton("alpha"));
assertThat(bravo.getDependsOn()).containsExactly("alpha");
});
}
@Test
void whenDependenciesAreConfiguredDetectedDatabaseInitializersAreInitializedInCorrectOrder() {
BeanDefinition alpha = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition bravo1 = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition bravo2 = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition charlie = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition delta = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
performDetection(
Arrays.asList(MockDatabaseInitializerDetector.class, OrderedLowestMockDatabaseInitializerDetector.class,
OrderedNearLowestMockDatabaseInitializerDetector.class,
MockedDependsOnDatabaseInitializationDetector.class),
(context) -> {
given(MockDatabaseInitializerDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("alpha"));
given(OrderedNearLowestMockDatabaseInitializerDetector.instance.detect(context.getBeanFactory()))
.willReturn(new LinkedHashSet<>(Arrays.asList("bravo1", "bravo2")));
given(OrderedLowestMockDatabaseInitializerDetector.instance.detect(context.getBeanFactory()))
.willReturn(new LinkedHashSet<>(Arrays.asList("charlie")));
given(MockedDependsOnDatabaseInitializationDetector.instance.detect(context.getBeanFactory()))
.willReturn(Collections.singleton("delta"));
context.registerBeanDefinition("alpha", alpha);
context.registerBeanDefinition("bravo1", bravo1);
context.registerBeanDefinition("bravo2", bravo2);
context.registerBeanDefinition("charlie", charlie);
context.registerBeanDefinition("delta", delta);
context.register(DependencyConfigurerConfiguration.class);
context.refresh();
assertThat(delta.getDependsOn()).containsExactlyInAnyOrder("alpha", "bravo1", "bravo2", "charlie");
assertThat(charlie.getDependsOn()).containsExactly("bravo1", "bravo2");
assertThat(bravo1.getDependsOn()).containsExactly("alpha");
assertThat(bravo2.getDependsOn()).containsExactly("alpha");
assertThat(alpha.getDependsOn()).isNullOrEmpty();
});
}
@Test
void whenInAnAotProcessedContextDependsOnDatabaseInitializationPostProcessorDoesNothing() {
withAotEnabled(() -> {
BeanDefinition alpha = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
BeanDefinition bravo = BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition();
performDetection(Arrays.asList(MockDatabaseInitializerDetector.class,
MockedDependsOnDatabaseInitializationDetector.class), (context) -> {
context.registerBeanDefinition("alpha", alpha);
context.registerBeanDefinition("bravo", bravo);
context.register(DependencyConfigurerConfiguration.class);
context.refresh();
assertThat(alpha.getAttribute(DatabaseInitializerDetector.class.getName())).isNull();
assertThat(bravo.getAttribute(DatabaseInitializerDetector.class.getName())).isNull();
then(MockDatabaseInitializerDetector.instance).shouldHaveNoInteractions();
assertThat(bravo.getDependsOn()).isNull();
});
});
}
private void withAotEnabled(Runnable action) {
System.setProperty("spring.aot.enabled", "true");
try {
action.run();
}
finally {
System.clearProperty("spring.aot.enabled");
}
}
private void performDetection(Collection<Class<?>> detectors,
Consumer<AnnotationConfigApplicationContext> contextCallback) {
DetectorSpringFactoriesClassLoader detectorSpringFactories = new DetectorSpringFactoriesClassLoader(this.temp);
detectors.forEach(detectorSpringFactories::register);
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.setEnvironment(this.environment);
context.setClassLoader(detectorSpringFactories);
contextCallback.accept(context);
}
}
@Configuration(proxyBeanMethods = false)
@Import(DatabaseInitializationDependencyConfigurer.class)
static class DependencyConfigurerConfiguration {
}
static class ConstructorInjectionDatabaseInitializerDetector implements DatabaseInitializerDetector {
private static Environment environment;
ConstructorInjectionDatabaseInitializerDetector(Environment environment) {
ConstructorInjectionDatabaseInitializerDetector.environment = environment;
}
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return Collections.singleton("alpha");
}
}
static class ConstructorInjectionDependsOnDatabaseInitializationDetector
implements DependsOnDatabaseInitializationDetector {
private static Environment environment;
ConstructorInjectionDependsOnDatabaseInitializationDetector(Environment environment) {
ConstructorInjectionDependsOnDatabaseInitializationDetector.environment = environment;
}
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return Collections.emptySet();
}
}
static class MockDatabaseInitializerDetector implements DatabaseInitializerDetector {
private static final DatabaseInitializerDetector instance = mock(DatabaseInitializerDetector.class);
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return instance.detect(beanFactory);
}
@Override
public void detectionComplete(ConfigurableListableBeanFactory beanFactory,
Set<String> databaseInitializerNames) {
instance.detectionComplete(beanFactory, databaseInitializerNames);
}
}
static class OrderedLowestMockDatabaseInitializerDetector implements DatabaseInitializerDetector {
private static final DatabaseInitializerDetector instance = mock(DatabaseInitializerDetector.class);
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return instance.detect(beanFactory);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
static class OrderedNearLowestMockDatabaseInitializerDetector implements DatabaseInitializerDetector {
private static final DatabaseInitializerDetector instance = mock(DatabaseInitializerDetector.class);
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return instance.detect(beanFactory);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
}
static class MockedDependsOnDatabaseInitializationDetector implements DependsOnDatabaseInitializationDetector {
private static final DependsOnDatabaseInitializationDetector instance = mock(
DependsOnDatabaseInitializationDetector.class);
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
return instance.detect(beanFactory);
}
}
static class DetectorSpringFactoriesClassLoader extends ClassLoader {
private final Set<Class<DatabaseInitializerDetector>> databaseInitializerDetectors = new HashSet<>();
private final Set<Class<DependsOnDatabaseInitializationDetector>> dependsOnDatabaseInitializationDetectors = new HashSet<>();
private final File temp;
DetectorSpringFactoriesClassLoader(File temp) {
this.temp = temp;
}
@SuppressWarnings("unchecked")
void register(Class<?> detector) {
if (DatabaseInitializerDetector.class.isAssignableFrom(detector)) {
this.databaseInitializerDetectors.add((Class<DatabaseInitializerDetector>) detector);
}
else if (DependsOnDatabaseInitializationDetector.class.isAssignableFrom(detector)) {
this.dependsOnDatabaseInitializationDetectors
.add((Class<DependsOnDatabaseInitializationDetector>) detector);
}
else {
throw new IllegalArgumentException("Unsupported detector type '" + detector.getName() + "'");
}
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (!"META-INF/spring.factories".equals(name)) {
return super.findResources(name);
}
Properties properties = new Properties();
properties.put(DatabaseInitializerDetector.class.getName(),
String.join(",", this.databaseInitializerDetectors.stream().map(Class::getName).toList()));
properties.put(DependsOnDatabaseInitializationDetector.class.getName(), String.join(",",
this.dependsOnDatabaseInitializationDetectors.stream().map(Class::getName).toList()));
File springFactories = new File(this.temp, "spring.factories");
try (FileWriter writer = new FileWriter(springFactories)) {
properties.store(writer, "");
}
return Collections.enumeration(Collections.singleton(springFactories.toURI().toURL()));
}
}
@Configuration(proxyBeanMethods = false)
static class DependsOnCaptor {
static final Map<String, List<String>> dependsOn = new HashMap<>();
@Bean
static BeanFactoryPostProcessor dependsOnCapturingPostProcessor() {
return (beanFactory) -> {
dependsOn.clear();
for (String name : beanFactory.getBeanDefinitionNames()) {
storeDependsOn(name, beanFactory);
}
};
}
private static void storeDependsOn(String name, ConfigurableListableBeanFactory beanFactory) {
String[] dependsOn = beanFactory.getBeanDefinition(name).getDependsOn();
if (dependsOn != null) {
DependsOnCaptor.dependsOn.put(name, Arrays.asList(dependsOn));
}
}
}
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2012-2025 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.sql.init;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.dao.DataAccessException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Base class for testing {@link AbstractScriptDatabaseInitializer} implementations.
*
* @param <T> type of the initializer being tested
* @author Andy Wilkinson
*/
public abstract class AbstractScriptDatabaseInitializerTests<T extends AbstractScriptDatabaseInitializer> {
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenDatabaseIsInitializedThenSchemaAndDataScriptsAreApplied() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfEmbeddedRows("SELECT COUNT(*) FROM EXAMPLE")).isOne();
}
@Test
void whenDatabaseIsInitializedWithDirectoryLocationsThenFailureIsHelpful() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("/org/springframework/boot/sql/init"));
settings.setDataLocations(Arrays.asList("/org/springframework/boot/sql/init"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase)
.withMessage("No schema scripts found at location '/org/springframework/boot/sql/init'");
}
@Test
@WithDataSqlResource
void whenContinueOnErrorIsFalseThenInitializationFailsOnError() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("data.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThatExceptionOfType(DataAccessException.class).isThrownBy(initializer::initializeDatabase);
assertThatDatabaseWasAccessed(initializer);
}
@Test
@WithDataSqlResource
void whenContinueOnErrorIsTrueThenInitializationDoesNotFailOnError() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setContinueOnError(true);
settings.setDataLocations(Arrays.asList("data.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThatDatabaseWasAccessed(initializer);
}
@Test
void whenNoScriptsExistAtASchemaLocationThenInitializationFails() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("does-not-exist.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase)
.withMessage("No schema scripts found at location 'does-not-exist.sql'");
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
void whenNoScriptsExistAtADataLocationThenInitializationFails() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("does-not-exist.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThatIllegalStateException().isThrownBy(initializer::initializeDatabase)
.withMessage("No data scripts found at location 'does-not-exist.sql'");
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
void whenNoScriptsExistAtAnOptionalSchemaLocationThenDatabaseIsNotAccessed() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("optional:does-not-exist.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse();
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
void whenNoScriptsExistAtAnOptionalDataLocationThenDatabaseIsNotAccessed() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setDataLocations(Arrays.asList("optional:does-not-exist.sql"));
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse();
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsNeverThenEmbeddedDatabaseIsNotInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.NEVER);
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse();
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsNeverThenStandaloneDatabaseIsNotInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.NEVER);
T initializer = createStandaloneDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse();
assertThatDatabaseWasNotAccessed(initializer);
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsEmbeddedThenEmbeddedDatabaseIsInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.EMBEDDED);
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfEmbeddedRows("SELECT COUNT(*) FROM EXAMPLE")).isOne();
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsEmbeddedThenStandaloneDatabaseIsNotInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.EMBEDDED);
T initializer = createStandaloneDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isFalse();
assertThatDatabaseWasAccessed(initializer);
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsAlwaysThenEmbeddedDatabaseIsInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.ALWAYS);
T initializer = createEmbeddedDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfEmbeddedRows("SELECT COUNT(*) FROM EXAMPLE")).isOne();
}
@Test
@WithSchemaSqlResource
@WithDataSqlResource
void whenModeIsAlwaysThenStandaloneDatabaseIsInitialized() {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(Arrays.asList("schema.sql"));
settings.setDataLocations(Arrays.asList("data.sql"));
settings.setMode(DatabaseInitializationMode.ALWAYS);
T initializer = createStandaloneDatabaseInitializer(settings);
assertThat(initializer.initializeDatabase()).isTrue();
assertThat(numberOfStandaloneRows("SELECT COUNT(*) FROM EXAMPLE")).isOne();
}
protected abstract T createStandaloneDatabaseInitializer(DatabaseInitializationSettings settings);
protected abstract T createEmbeddedDatabaseInitializer(DatabaseInitializationSettings settings);
protected abstract int numberOfEmbeddedRows(String sql);
protected abstract int numberOfStandaloneRows(String sql);
private void assertThatDatabaseWasAccessed(T initializer) {
assertDatabaseAccessed(true, initializer);
}
private void assertThatDatabaseWasNotAccessed(T initializer) {
assertDatabaseAccessed(false, initializer);
}
protected abstract void assertDatabaseAccessed(boolean accessed, T initializer);
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@WithResource(name = "schema.sql", content = """
CREATE TABLE EXAMPLE (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(30)
);
""")
protected @interface WithSchemaSqlResource {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@WithResource(name = "data.sql", content = "INSERT INTO EXAMPLE VALUES (1, 'Andy');")
protected @interface WithDataSqlResource {
}
}