Commit 2f83a671 authored by Andy Wilkinson's avatar Andy Wilkinson

Rework DataSource initialization

Previously, DataSource initialization was triggered via a
BeanPostProcessor or a schema created event from JPA. This caused
numerous problems with circular dependencies, bean lifecycle, etc and
added significant complexity.

This commit reworks DataSource initialization to remove the use of a
BeanPostProcessor entirely. In its place, DataSource initialization is
now driven by an InitializingBean with dependency relationships
between beans ensuring that initialization has been performed before
the DataSource is used. This aligns with the approach that's worked
well with Flyway and Liquibase.

More changes are planned to further simplify DataSource initialization.
The changes in this commit are a foundation for those changes. Any new
public API in this commit is highly likely to change before the next
GA.

Fixes gh-13042
Fixes gh-23736
parent b3d73a1d
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,44 +18,44 @@ package org.springframework.boot.autoconfigure.jdbc; ...@@ -18,44 +18,44 @@ package org.springframework.boot.autoconfigure.jdbc;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ResourceLoaderAware;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.io.ResourceLoader;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
/** /**
* {@link BeanPostProcessor} used to ensure that {@link DataSourceInitializer} is * {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* initialized as soon as a {@link DataSource} is. * DML scripts.
* *
* @author Dave Syer * @author Andy Wilkinson
* @since 2.5.0
*/ */
class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered, BeanFactoryAware { public class DataSourceInitialization implements InitializingBean, ResourceLoaderAware {
@Override private final DataSource dataSource;
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
private BeanFactory beanFactory; private final DataSourceProperties properies;
@Override private volatile ResourceLoader resourceLoader;
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; /**
* 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 @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { public void afterPropertiesSet() throws Exception {
if (bean instanceof DataSource) { new DataSourceInitializer(this.dataSource, this.properies, this.resourceLoader).initializeDataSource();
// force initialization of this bean as soon as we see a DataSource
this.beanFactory.getBean(DataSourceInitializerInvoker.class);
}
return bean;
} }
@Override @Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException { public void setResourceLoader(ResourceLoader resourceLoader) {
this.beanFactory = beanFactory; this.resourceLoader = resourceLoader;
} }
} }
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,47 +16,82 @@ ...@@ -16,47 +16,82 @@
package org.springframework.boot.autoconfigure.jdbc; package org.springframework.boot.autoconfigure.jdbc;
import org.springframework.beans.factory.config.BeanDefinition; import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition; import javax.sql.DataSource;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
/** /**
* Configures DataSource initialization. * Configuration for {@link DataSource} initialization using DDL and DML scripts.
* *
* @author Stephane Nicoll * @author Andy Wilkinson
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class }) @ConditionalOnSingleCandidate(DataSource.class)
class DataSourceInitializationConfiguration { class DataSourceInitializationConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.datasource", name = "initialization-order", havingValue = "before-jpa",
matchIfMissing = true)
@Import({ DataSourceInitializationJdbcOperationsDependsOnPostProcessor.class,
DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor.class,
DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor.class })
static class BeforeJpaDataSourceInitializationConfiguration {
@Bean
DataSourceInitialization dataSourceInitialization(DataSource dataSource, DataSourceProperties properties) {
return new DataSourceInitialization(dataSource, properties);
}
}
/** /**
* {@link ImportBeanDefinitionRegistrar} to register the * Post processor to ensure that {@link EntityManagerFactory} beans depend on any
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation * {@link DataSourceInitialization} beans.
* issues.
*/ */
static class Registrar implements ImportBeanDefinitionRegistrar { @ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
static class DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor
private static final String BEAN_NAME = "dataSourceInitializerPostProcessor"; extends EntityManagerFactoryDependsOnPostProcessor {
@Override DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor() {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, super(DataSourceInitialization.class);
BeanDefinitionRegistry registry) { }
if (!registry.containsBeanDefinition(BEAN_NAME)) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder }
.genericBeanDefinition(DataSourceInitializerPostProcessor.class,
DataSourceInitializerPostProcessor::new) /**
.getBeanDefinition(); * Post processor to ensure that {@link JdbcOperations} beans depend on any
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); * {@link DataSourceInitialization} beans.
// We don't need this one to be post processed otherwise it can cause a */
// cascade of bean instantiation that we would rather avoid. @ConditionalOnClass(JdbcOperations.class)
beanDefinition.setSynthetic(true); static class DataSourceInitializationJdbcOperationsDependsOnPostProcessor
registry.registerBeanDefinition(BEAN_NAME, beanDefinition); extends JdbcOperationsDependsOnPostProcessor {
}
DataSourceInitializationJdbcOperationsDependsOnPostProcessor() {
super(DataSourceInitialization.class);
}
}
/**
* Post processor to ensure that {@link NamedParameterJdbcOperations} beans depend on
* any {@link DataSourceInitialization} beans.
*/
@ConditionalOnClass(NamedParameterJdbcOperations.class)
protected static class DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor
extends NamedParameterJdbcOperationsDependsOnPostProcessor {
public DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor() {
super(DataSourceInitialization.class);
} }
} }
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -46,8 +46,9 @@ import org.springframework.util.StringUtils; ...@@ -46,8 +46,9 @@ import org.springframework.util.StringUtils;
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Kazuki Shimizu * @author Kazuki Shimizu
* @since 2.5.0
*/ */
class DataSourceInitializer { public class DataSourceInitializer {
private static final Log logger = LogFactory.getLog(DataSourceInitializer.class); private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);
...@@ -64,32 +65,25 @@ class DataSourceInitializer { ...@@ -64,32 +65,25 @@ class DataSourceInitializer {
* @param properties the matching configuration * @param properties the matching configuration
* @param resourceLoader the resource loader to use (can be null) * @param resourceLoader the resource loader to use (can be null)
*/ */
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties, ResourceLoader resourceLoader) { public DataSourceInitializer(DataSource dataSource, DataSourceProperties properties,
ResourceLoader resourceLoader) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.properties = properties; this.properties = properties;
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null); this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
} }
/** /**
* Create a new instance with the {@link DataSource} to initialize and its matching * Initializes the {@link DataSource} by running DDL and DML scripts.
* {@link DataSourceProperties configuration}. * @return {@code true} if one or more scripts were applied to the database, otherwise
* @param dataSource the datasource to initialize * {@code false}
* @param properties the matching configuration
*/ */
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties) { public boolean initializeDataSource() {
this(dataSource, properties, null); boolean initialized = createSchema();
} initialized = initSchema() && initialized;
return initialized;
DataSource getDataSource() {
return this.dataSource;
} }
/** private boolean createSchema() {
* Create the schema if necessary.
* @return {@code true} if the schema was created
* @see DataSourceProperties#getSchema()
*/
boolean createSchema() {
List<Resource> scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema"); List<Resource> scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) { if (!scripts.isEmpty()) {
if (!isEnabled()) { if (!isEnabled()) {
...@@ -103,21 +97,18 @@ class DataSourceInitializer { ...@@ -103,21 +97,18 @@ class DataSourceInitializer {
return !scripts.isEmpty(); return !scripts.isEmpty();
} }
/** private boolean initSchema() {
* Initialize the schema if necessary.
* @see DataSourceProperties#getData()
*/
void initSchema() {
List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data"); List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
if (!scripts.isEmpty()) { if (!scripts.isEmpty()) {
if (!isEnabled()) { if (!isEnabled()) {
logger.debug("Initialization disabled (not running data scripts)"); logger.debug("Initialization disabled (not running data scripts)");
return; return false;
} }
String username = this.properties.getDataUsername(); String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword(); String password = this.properties.getDataPassword();
runScripts(scripts, username, password); runScripts(scripts, username, password);
} }
return !scripts.isEmpty();
} }
private boolean isEnabled() { private boolean isEnabled() {
......
/*
* Copyright 2012-2019 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.log.LogMessage;
/**
* Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on
* {@link InitializingBean#afterPropertiesSet()} and {@literal data-*.sql} SQL scripts on
* a {@link DataSourceSchemaCreatedEvent}.
*
* @author Stephane Nicoll
* @see DataSourceAutoConfiguration
*/
class DataSourceInitializerInvoker implements ApplicationListener<DataSourceSchemaCreatedEvent>, InitializingBean {
private static final Log logger = LogFactory.getLog(DataSourceInitializerInvoker.class);
private final ObjectProvider<DataSource> dataSource;
private final DataSourceProperties properties;
private final ApplicationContext applicationContext;
private DataSourceInitializer dataSourceInitializer;
private boolean initialized;
DataSourceInitializerInvoker(ObjectProvider<DataSource> dataSource, DataSourceProperties properties,
ApplicationContext applicationContext) {
this.dataSource = dataSource;
this.properties = properties;
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
DataSourceInitializer initializer = getDataSourceInitializer();
if (initializer != null) {
boolean schemaCreated = this.dataSourceInitializer.createSchema();
if (schemaCreated) {
initialize(initializer);
}
}
}
private void initialize(DataSourceInitializer initializer) {
try {
this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer.getDataSource()));
// The listener might not be registered yet, so don't rely on it.
if (!this.initialized) {
this.dataSourceInitializer.initSchema();
this.initialized = true;
}
}
catch (IllegalStateException ex) {
logger.warn(LogMessage.format("Could not send event to complete DataSource initialization (%s)",
ex.getMessage()));
}
}
@Override
public void onApplicationEvent(DataSourceSchemaCreatedEvent event) {
// NOTE the event can happen more than once and
// the event datasource is not used here
DataSourceInitializer initializer = getDataSourceInitializer();
if (!this.initialized && initializer != null) {
initializer.initSchema();
this.initialized = true;
}
}
private DataSourceInitializer getDataSourceInitializer() {
if (this.dataSourceInitializer == null) {
DataSource ds = this.dataSource.getIfUnique();
if (ds != null) {
this.dataSourceInitializer = new DataSourceInitializer(ds, this.properties, this.applicationContext);
}
}
return this.dataSourceInitializer;
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -28,8 +28,10 @@ import org.springframework.context.ApplicationEvent; ...@@ -28,8 +28,10 @@ import org.springframework.context.ApplicationEvent;
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 2.0.0 * @since 2.0.0
* @deprecated since 2.5.0 with no replacement as the event is no longer published
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Deprecated
public class DataSourceSchemaCreatedEvent extends ApplicationEvent { public class DataSourceSchemaCreatedEvent extends ApplicationEvent {
/** /**
......
/*
* 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.sql.DataSource;
import org.springframework.context.annotation.DependsOn;
/**
* Annotation used to indicate that a bean depends upon the auto-configured DataSource
* initialization having completed before the {@link DataSource} is injected. If the
* DataSource is not used during startup, no such dependency is required.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DependsOn("dataSourceInitialization")
public @interface DependsOnDataSourceInitialization {
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,16 +17,23 @@ ...@@ -17,16 +17,23 @@
package org.springframework.boot.autoconfigure.orm.jpa; package org.springframework.boot.autoconfigure.orm.jpa;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.core.io.ResourceLoader;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
/** /**
...@@ -45,4 +52,18 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; ...@@ -45,4 +52,18 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
@Import(HibernateJpaConfiguration.class) @Import(HibernateJpaConfiguration.class)
public class HibernateJpaAutoConfiguration { public class HibernateJpaAutoConfiguration {
@ConditionalOnProperty(prefix = "spring.datasource", name = "initialization-order", havingValue = "after-jpa",
matchIfMissing = false)
static class AfterJpaDataSourceInitializationConfiguration {
@Bean
HibernatePropertiesCustomizer dataSourceInitializationCustomizer(DataSource dataSource,
DataSourceProperties properties, ResourceLoader resourceLoader) {
return (hibernateProperties) -> hibernateProperties.put(AvailableSettings.SCHEMA_MANAGEMENT_TOOL,
new SpringBootSchemaManagementTool(
new DataSourceInitializer(dataSource, properties, resourceLoader)));
}
}
} }
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -44,7 +44,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties ...@@ -44,7 +44,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.JpaVendorAdapter;
...@@ -74,7 +73,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; ...@@ -74,7 +73,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class) @EnableConfigurationProperties(JpaProperties.class)
@Import(DataSourceInitializedPublisher.Registrar.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware { public abstract class JpaBaseConfiguration implements BeanFactoryAware {
private final DataSource dataSource; private final DataSource dataSource;
......
/*
* 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.orm.jpa;
import org.hibernate.boot.Metadata;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.resource.transaction.spi.DdlTransactionIsolator;
import org.hibernate.tool.schema.TargetType;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.internal.exec.GenerationTarget;
import org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase;
import org.hibernate.tool.schema.internal.exec.JdbcContext;
import org.hibernate.tool.schema.spi.ExecutionOptions;
import org.hibernate.tool.schema.spi.SchemaCreator;
import org.hibernate.tool.schema.spi.SourceDescriptor;
import org.hibernate.tool.schema.spi.TargetDescriptor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer;
/**
* Spring Boot {@link SchemaCreator}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class SpringBootSchemaCreator implements SchemaCreator {
private static final CoreMessageLogger log = CoreLogging.messageLogger(SpringBootSchemaCreator.class);
private final HibernateSchemaManagementTool tool;
private final DataSourceInitializer dataSourceInitializer;
private final SchemaCreator creator;
SpringBootSchemaCreator(HibernateSchemaManagementTool tool, SchemaCreator creator,
DataSourceInitializer dataSourceInitializer) {
this.tool = tool;
this.creator = creator;
this.dataSourceInitializer = dataSourceInitializer;
}
@Override
public void doCreation(Metadata metadata, ExecutionOptions options, SourceDescriptor sourceDescriptor,
TargetDescriptor targetDescriptor) {
if (!targetDescriptor.getTargetTypes().contains(TargetType.DATABASE)) {
this.creator.doCreation(metadata, options, sourceDescriptor, targetDescriptor);
return;
}
GenerationTarget databaseTarget = getDatabaseTarget(options, targetDescriptor);
databaseTarget.prepare();
try {
this.creator.doCreation(metadata, options, sourceDescriptor, targetDescriptor);
this.dataSourceInitializer.initializeDataSource();
}
finally {
try {
databaseTarget.release();
}
catch (Exception ex) {
log.debugf("Problem releasing GenerationTarget [%s] : %s", databaseTarget, ex.getMessage());
}
}
}
private GenerationTarget getDatabaseTarget(ExecutionOptions options, TargetDescriptor targetDescriptor) {
JdbcContext jdbcContext = this.tool.resolveJdbcContext(options.getConfigurationValues());
DdlTransactionIsolator ddlTransactionIsolator = this.tool.getDdlTransactionIsolator(jdbcContext);
return new GenerationTargetToDatabase(ddlTransactionIsolator);
}
}
/*
* 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.orm.jpa;
import java.util.Map;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.spi.SchemaCreator;
import org.hibernate.tool.schema.spi.SchemaManagementTool;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer;
/**
* Spring Boot {@link SchemaManagementTool}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class SpringBootSchemaManagementTool extends HibernateSchemaManagementTool {
private final DataSourceInitializer dataSourceInitializer;
SpringBootSchemaManagementTool(DataSourceInitializer dataSourceInitializer) {
this.dataSourceInitializer = dataSourceInitializer;
}
@Override
@SuppressWarnings("rawtypes")
public SchemaCreator getSchemaCreator(Map options) {
SchemaCreator creator = super.getSchemaCreator(options);
return new SpringBootSchemaCreator(this, creator, this.dataSourceInitializer);
}
}
...@@ -756,6 +756,10 @@ ...@@ -756,6 +756,10 @@
"name": "spring.datasource.initialization-mode", "name": "spring.datasource.initialization-mode",
"defaultValue": "embedded" "defaultValue": "embedded"
}, },
{
"name": "spring.datasource.initialization-order",
"defaultValue": "before-jpa"
},
{ {
"name": "spring.datasource.jmx-enabled", "name": "spring.datasource.jmx-enabled",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",
...@@ -1901,6 +1905,17 @@ ...@@ -1901,6 +1905,17 @@
} }
] ]
}, },
{
"name": "spring.datasource.initialization-order",
"values": [
{
"value": "before-jpa"
},
{
"value": "after-jpa"
}
]
},
{ {
"name": "spring.datasource.schema", "name": "spring.datasource.schema",
"providers": [ "providers": [
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -274,6 +274,7 @@ class DataSourceAutoConfigurationTests { ...@@ -274,6 +274,7 @@ class DataSourceAutoConfigurationTests {
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@DependsOnDataSourceInitialization
static class TestInitializedDataSourceConfiguration { static class TestInitializedDataSourceConfiguration {
private boolean called; private boolean called;
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,25 +17,36 @@ ...@@ -17,25 +17,36 @@
package org.springframework.boot.autoconfigure.jdbc; package org.springframework.boot.autoconfigure.jdbc;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import javax.sql.DataSource; import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.SimpleThreadScope;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
...@@ -50,12 +61,12 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -50,12 +61,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/** /**
* Tests for {@link DataSourceInitializerInvoker}. * Integration tests for DataSource initialization.
* *
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
class DataSourceInitializerInvokerTests { class DataSourceInitializationIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
...@@ -151,7 +162,6 @@ class DataSourceInitializerInvokerTests { ...@@ -151,7 +162,6 @@ class DataSourceInitializerInvokerTests {
return (context) -> { return (context) -> {
assertThat(context).hasSingleBean(DataSource.class); assertThat(context).hasSingleBean(DataSource.class);
DataSource dataSource = context.getBean(DataSource.class); DataSource dataSource = context.getBean(DataSource.class);
context.publishEvent(new DataSourceSchemaCreatedEvent(dataSource));
assertDataSourceNotInitialized(dataSource); assertDataSourceNotInitialized(dataSource);
}; };
} }
...@@ -238,6 +248,31 @@ class DataSourceInitializerInvokerTests { ...@@ -238,6 +248,31 @@ class DataSourceInitializerInvokerTests {
}); });
} }
@Test
void whenDataSourceIsProxiedByABeanPostProcessorThenDataSourceInitializationUsesTheProxy() {
this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always")
.withUserConfiguration(DataSourceProxyConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(DataSource.class);
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(DataSourceProxy.class);
assertThat(((DataSourceProxy) dataSource).connectionsRetrieved).hasPositiveValue();
assertDataSourceIsInitialized(dataSource);
});
}
@Test
// gh-13042
void whenDataSourceIsScopedAndJpaIsInvolvedThenInitializationCompletesSuccessfully() {
this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always")
.withConfiguration(AutoConfigurations.of(HibernateJpaAutoConfiguration.class))
.withUserConfiguration(ScopedDataSourceConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(DataSource.class);
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(HikariDataSource.class);
assertDataSourceIsInitialized(dataSource);
});
}
private String getRelativeLocationFor(String resource) { private String getRelativeLocationFor(String resource) {
return ClassUtils.addResourcePathToPackagePath(getClass(), resource); return ClassUtils.addResourcePathToPackagePath(getClass(), resource);
} }
...@@ -293,4 +328,99 @@ class DataSourceInitializerInvokerTests { ...@@ -293,4 +328,99 @@ class DataSourceInitializerInvokerTests {
} }
@Configuration(proxyBeanMethods = true)
static class DataSourceProxyConfiguration {
@Bean
static BeanPostProcessor dataSourceProxy() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DataSource) {
return new DataSourceProxy((DataSource) bean);
}
return bean;
}
};
}
}
static class DataSourceProxy implements DataSource {
private final AtomicInteger connectionsRetrieved = new AtomicInteger();
private final DataSource delegate;
DataSourceProxy(DataSource delegate) {
this.delegate = delegate;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return this.delegate.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
this.delegate.setLogWriter(out);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return this.delegate.isWrapperFor(iface);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return this.delegate.unwrap(iface);
}
@Override
public Connection getConnection() throws SQLException {
this.connectionsRetrieved.incrementAndGet();
return this.delegate.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
this.connectionsRetrieved.incrementAndGet();
return this.delegate.getConnection(username, password);
}
@Override
public int getLoginTimeout() throws SQLException {
return this.delegate.getLoginTimeout();
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
this.delegate.setLoginTimeout(seconds);
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return this.delegate.getParentLogger();
}
}
@Configuration(proxyBeanMethods = false)
static class ScopedDataSourceConfiguration {
@Bean
static BeanFactoryPostProcessor fooScope() {
return (beanFactory) -> beanFactory.registerScope("test", new SimpleThreadScope());
}
@Bean
@Scope("test")
HikariDataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
}
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -33,6 +33,7 @@ import org.springframework.jdbc.core.JdbcTemplate; ...@@ -33,6 +33,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
/** /**
...@@ -45,11 +46,9 @@ class DataSourceInitializerTests { ...@@ -45,11 +46,9 @@ class DataSourceInitializerTests {
@Test @Test
void initializeEmbeddedByDefault() { void initializeEmbeddedByDefault() {
try (HikariDataSource dataSource = createDataSource()) { try (HikariDataSource dataSource = createDataSource()) {
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
assertThat(initializer.createSchema()).isTrue(); initializer.initializeDataSource();
assertNumberOfRows(jdbcTemplate, 0);
initializer.initSchema();
assertNumberOfRows(jdbcTemplate, 1); assertNumberOfRows(jdbcTemplate, 1);
} }
} }
...@@ -59,11 +58,9 @@ class DataSourceInitializerTests { ...@@ -59,11 +58,9 @@ class DataSourceInitializerTests {
try (HikariDataSource dataSource = createDataSource()) { try (HikariDataSource dataSource = createDataSource()) {
DataSourceProperties properties = new DataSourceProperties(); DataSourceProperties properties = new DataSourceProperties();
properties.setInitializationMode(DataSourceInitializationMode.ALWAYS); properties.setInitializationMode(DataSourceInitializationMode.ALWAYS);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
assertThat(initializer.createSchema()).isTrue(); initializer.initializeDataSource();
assertNumberOfRows(jdbcTemplate, 0);
initializer.initSchema();
assertNumberOfRows(jdbcTemplate, 1); assertNumberOfRows(jdbcTemplate, 1);
} }
} }
...@@ -77,8 +74,8 @@ class DataSourceInitializerTests { ...@@ -77,8 +74,8 @@ class DataSourceInitializerTests {
try (HikariDataSource dataSource = createDataSource()) { try (HikariDataSource dataSource = createDataSource()) {
DataSourceProperties properties = new DataSourceProperties(); DataSourceProperties properties = new DataSourceProperties();
properties.setInitializationMode(DataSourceInitializationMode.NEVER); properties.setInitializationMode(DataSourceInitializationMode.NEVER);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null);
assertThat(initializer.createSchema()).isFalse(); assertThat(initializer.initializeDataSource()).isFalse();
} }
} }
...@@ -90,9 +87,9 @@ class DataSourceInitializerTests { ...@@ -90,9 +87,9 @@ class DataSourceInitializerTests {
given(connection.getMetaData()).willReturn(metadata); given(connection.getMetaData()).willReturn(metadata);
DataSource dataSource = mock(DataSource.class); DataSource dataSource = mock(DataSource.class);
given(dataSource.getConnection()).willReturn(connection); given(dataSource.getConnection()).willReturn(connection);
DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null);
assertThat(initializer.createSchema()).isFalse(); assertThat(initializer.initializeDataSource()).isFalse();
verify(dataSource).getConnection(); verify(dataSource, times(2)).getConnection();
} }
private HikariDataSource createDataSource() { private HikariDataSource createDataSource() {
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.orm.jpa; ...@@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.orm.jpa;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
...@@ -28,7 +27,6 @@ import java.util.HashMap; ...@@ -28,7 +27,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory; import javax.persistence.EntityManagerFactory;
...@@ -39,7 +37,6 @@ import javax.transaction.TransactionManager; ...@@ -39,7 +37,6 @@ import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction; import javax.transaction.UserTransaction;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.awaitility.Awaitility;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
...@@ -55,14 +52,15 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -55,14 +52,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener;
import org.springframework.boot.autoconfigure.orm.jpa.mapping.NonAnnotatedEntity; import org.springframework.boot.autoconfigure.orm.jpa.mapping.NonAnnotatedEntity;
import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.orm.jpa.test.City;
import org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform;
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy; import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
...@@ -72,6 +70,7 @@ import org.springframework.context.ApplicationEvent; ...@@ -72,6 +70,7 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
...@@ -79,8 +78,8 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; ...@@ -79,8 +78,8 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.entry;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
...@@ -124,7 +123,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -124,7 +123,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class)
.withClassLoader(new HideDataScriptClassLoader()) .withClassLoader(new HideDataScriptClassLoader())
.withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop",
"spring.datasource.data:classpath:/city.sql") "spring.datasource.data:classpath:/city.sql",
"spring.datasource.initialization-order=after-jpa")
.run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue()); .run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue());
} }
...@@ -286,7 +286,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -286,7 +286,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
void customResourceMapping() { void customResourceMapping() {
contextRunner().withClassLoader(new HideDataScriptClassLoader()) contextRunner().withClassLoader(new HideDataScriptClassLoader())
.withPropertyValues("spring.datasource.data:classpath:/db/non-annotated-data.sql", .withPropertyValues("spring.datasource.data:classpath:/db/non-annotated-data.sql",
"spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml") "spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml",
"spring.datasource.initialization-order=after-jpa")
.run((context) -> { .run((context) -> {
EntityManager em = context.getBean(EntityManagerFactory.class).createEntityManager(); EntityManager em = context.getBean(EntityManagerFactory.class).createEntityManager();
NonAnnotatedEntity found = em.find(NonAnnotatedEntity.class, 2000L); NonAnnotatedEntity found = em.find(NonAnnotatedEntity.class, 2000L);
...@@ -355,9 +356,12 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -355,9 +356,12 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
@Test @Test
void eventListenerCanBeRegisteredAsBeans() { void eventListenerCanBeRegisteredAsBeans() {
contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) contextRunner().withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO))
.withClassLoader(new HideDataScriptClassLoader()).withPropertyValues("spring.jpa.show-sql=true", .withUserConfiguration(TestInitializedJpaConfiguration.class)
"spring.jpa.hibernate.ddl-auto:create-drop", "spring.datasource.data:classpath:/city.sql") .withClassLoader(new HideDataScriptClassLoader())
.withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop",
"spring.datasource.data:classpath:/city.sql",
"spring.datasource.initialization-order=after-jpa")
.run((context) -> { .run((context) -> {
// See CityListener // See CityListener
assertThat(context).hasSingleBean(City.class); assertThat(context).hasSingleBean(City.class);
...@@ -376,8 +380,10 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -376,8 +380,10 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
contextRunner().withUserConfiguration(JpaUsingApplicationListenerConfiguration.class) contextRunner().withUserConfiguration(JpaUsingApplicationListenerConfiguration.class)
.withPropertyValues("spring.datasource.initialization-mode=never").run((context) -> { .withPropertyValues("spring.datasource.initialization-mode=never").run((context) -> {
assertThat(context).hasNotFailed(); assertThat(context).hasNotFailed();
assertThat(context.getBean(EventCapturingApplicationListener.class).events.stream() EventCapturingApplicationListener listener = context
.filter(DataSourceSchemaCreatedEvent.class::isInstance)).hasSize(1); .getBean(EventCapturingApplicationListener.class);
assertThat(listener.events).hasSize(1);
assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class);
}); });
} }
...@@ -390,8 +396,11 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -390,8 +396,11 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
assertThat(context).hasNotFailed(); assertThat(context).hasNotFailed();
EventCapturingApplicationListener listener = context EventCapturingApplicationListener listener = context
.getBean(EventCapturingApplicationListener.class); .getBean(EventCapturingApplicationListener.class);
Awaitility.waitAtMost(Duration.ofSeconds(30)) assertThat(listener.events).hasSize(1);
.until(() -> dataSourceSchemaCreatedEventsReceivedBy(listener), hasSize(1)); assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class);
// createEntityManager requires Hibernate bootstrapping to be complete
assertThatNoException()
.isThrownBy(() -> context.getBean(EntityManagerFactory.class).createEntityManager());
}); });
} }
...@@ -407,11 +416,6 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes ...@@ -407,11 +416,6 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
}); });
} }
private List<ApplicationEvent> dataSourceSchemaCreatedEventsReceivedBy(EventCapturingApplicationListener listener) {
return listener.events.stream().filter(DataSourceSchemaCreatedEvent.class::isInstance)
.collect(Collectors.toList());
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@TestAutoConfigurationPackage(City.class) @TestAutoConfigurationPackage(City.class)
static class TestInitializedJpaConfiguration { static class TestInitializedJpaConfiguration {
......
...@@ -2029,6 +2029,11 @@ In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{pl ...@@ -2029,6 +2029,11 @@ In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{pl
This allows you to switch to database-specific scripts if necessary. This allows you to switch to database-specific scripts if necessary.
For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on). For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on).
By default, script-based `DataSource` initialization is performed before any JPA `EntityManagerFactory` beans are created.
`schema.sql` can be used to create the schema for JPA-managed entities and `data.sql` can be used to populate it.
If you want script-based `DataSource` initialization to be performed after the `EntityManagerFactory`, set `spring.datasource.initialization-order` to `after-jpa`.
`schema.sql` can then be used to make additions to any schema creation performed by Hibernate and `data.sql` can be used to populate it.
[NOTE] [NOTE]
==== ====
When only basic SQL scripts are used, Spring Boot automatically creates the schema of an embedded `DataSource`. When only basic SQL scripts are used, Spring Boot automatically creates the schema of an embedded `DataSource`.
......
spring.test.mockmvc.print=none spring.test.mockmvc.print=none
spring.datasource.initialization-order=after-jpa
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment