Defer SQL initialization to fit with JPA better

Added 2 new spring.datasource.* properties ("data" like
"schema", and "deferDdl" like the "spring.jpa.hibernate.*"
flag). The SQL scripts are then run separately and the "data"
ones are triggered by a new DataSourceInitializedEvent,
which is also published by the Hibernate DDL schema export.

Fixes gh-1006
This commit is contained in:
Dave Syer 2014-06-02 13:07:56 +01:00
parent efcbb32788
commit 49a09c807c
9 changed files with 245 additions and 73 deletions

View File

@ -16,16 +16,8 @@
package org.springframework.boot.autoconfigure.jdbc;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
@ -38,23 +30,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link DataSource}.
@ -64,73 +51,18 @@ import org.springframework.util.StringUtils;
*/
@Configuration
@ConditionalOnClass(EmbeddedDatabaseType.class)
@Import(DataSourceInitialization.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
private static Log logger = LogFactory.getLog(DataSourceAutoConfiguration.class);
public static final String CONFIGURATION_PREFIX = "spring.datasource";
@Autowired(required = false)
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private DataSourceProperties properties;
@PostConstruct
protected void initialize() {
boolean initialize = this.properties.isInitialize();
if (this.dataSource == null || !initialize) {
logger.debug("No DataSource found so not initializing");
return;
}
String schema = this.properties.getSchema();
if (schema == null) {
String platform = this.properties.getPlatform();
schema = "classpath*:schema-" + platform + ".sql,";
schema += "classpath*:schema.sql,";
schema += "classpath*:data-" + platform + ".sql,";
schema += "classpath*:data.sql";
}
List<Resource> resources = getSchemaResources(schema);
boolean continueOnError = this.properties.isContinueOnError();
boolean exists = false;
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
for (Resource resource : resources) {
if (resource.exists()) {
exists = true;
populator.addScript(resource);
populator.setContinueOnError(continueOnError);
}
}
populator.setSeparator(this.properties.getSeparator());
if (exists) {
DatabasePopulatorUtils.execute(populator, this.dataSource);
}
}
private List<Resource> getSchemaResources(String schema) {
List<Resource> resources = new ArrayList<Resource>();
for (String schemaLocation : StringUtils.commaDelimitedListToStringArray(schema)) {
try {
resources.addAll(Arrays.asList(this.applicationContext
.getResources(schemaLocation)));
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load resource from "
+ schemaLocation, ex);
}
}
return resources;
}
/**
* Determines if the {@code dataSource} being used by Spring was created from
* {@link EmbeddedDataSourceConfiguration}.

View File

@ -0,0 +1,181 @@
/*
* Copyright 2012-2013 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
*
* http://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.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*/
@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceInitialization implements
ApplicationListener<ContextRefreshedEvent> {
private static Log logger = LogFactory.getLog(DataSourceAutoConfiguration.class);
@Autowired(required = false)
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private DataSourceProperties properties;
private boolean initialized = false;
@Bean
public ApplicationListener<DataSourceInitializedEvent> dataSourceInitializedListener() {
return new DataSourceInitializedListener();
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (this.properties.isDeferDdl()) {
boolean initialize = this.properties.isInitialize();
if (!initialize) {
logger.debug("Initialization disabled (not running DDL scripts)");
return;
}
runSchemaScripts();
}
}
@PostConstruct
protected void initialize() {
if (!this.properties.isDeferDdl()) {
boolean initialize = this.properties.isInitialize();
if (!initialize) {
logger.debug("Initialization disabled (not running DDL scripts)");
return;
}
runSchemaScripts();
}
}
private void runSchemaScripts() {
String schema = this.properties.getSchema();
if (schema == null) {
String platform = this.properties.getPlatform();
schema = "classpath*:schema-" + platform + ".sql,";
schema += "classpath*:schema.sql";
}
if (runScripts(schema)) {
this.applicationContext.publishEvent(new DataSourceInitializedEvent(
this.dataSource));
}
}
private void runDataScripts() {
if (this.initialized) {
return;
}
String schema = this.properties.getData();
if (schema == null) {
String platform = this.properties.getPlatform();
schema = "classpath*:data-" + platform + ".sql,";
schema += "classpath*:data.sql";
}
runScripts(schema);
this.initialized = true;
}
private boolean runScripts(String scripts) {
if (this.dataSource == null) {
logger.debug("No DataSource found so not initializing");
return false;
}
List<Resource> resources = getSchemaResources(scripts);
boolean continueOnError = this.properties.isContinueOnError();
boolean exists = false;
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
for (Resource resource : resources) {
if (resource.exists()) {
exists = true;
populator.addScript(resource);
populator.setContinueOnError(continueOnError);
}
}
populator.setSeparator(this.properties.getSeparator());
if (exists) {
DatabasePopulatorUtils.execute(populator, this.dataSource);
}
return exists;
}
private List<Resource> getSchemaResources(String schema) {
List<Resource> resources = new ArrayList<Resource>();
for (String schemaLocation : StringUtils.commaDelimitedListToStringArray(schema)) {
try {
resources.addAll(Arrays.asList(this.applicationContext
.getResources(schemaLocation)));
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load resource from "
+ schemaLocation, ex);
}
}
return resources;
}
public static class DataSourceInitializedEvent extends ApplicationEvent {
public DataSourceInitializedEvent(DataSource source) {
super(source);
}
}
private class DataSourceInitializedListener implements
ApplicationListener<DataSourceInitializedEvent> {
@Override
public void onApplicationEvent(DataSourceInitializedEvent event) {
runDataScripts();
}
}
}

View File

@ -46,10 +46,14 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
private boolean initialize = true;
private boolean deferDdl = false;
private String platform = "all";
private String schema;
private String data;
private boolean continueOnError = false;
private String separator = ";";
@ -154,6 +158,14 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
this.initialize = initialize;
}
public void setDeferDdl(boolean deferDdl) {
this.deferDdl = deferDdl;
}
public boolean isDeferDdl() {
return this.deferDdl;
}
public String getPlatform() {
return this.platform;
}
@ -170,6 +182,14 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
this.schema = schema;
}
public String getData() {
return this.data;
}
public void setData(String script) {
this.data = script;
}
public boolean isContinueOnError() {
return this.continueOnError;
}

View File

@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitialization.DataSourceInitializedEvent;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder.EntityManagerFactoryBeanCallback;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.HibernateEntityManagerCondition;
import org.springframework.context.ApplicationListener;
@ -90,7 +91,7 @@ public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration {
};
}
private static class DeferredSchemaAction implements
private class DeferredSchemaAction implements
ApplicationListener<ContextRefreshedEvent> {
private Map<String, String> map;
@ -105,11 +106,14 @@ public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
String ddlAuto = this.map.get("hibernate.hbm2ddl.auto");
if (ddlAuto == null || "none".equals(ddlAuto)) {
if (ddlAuto == null || "none".equals(ddlAuto) || "".equals(ddlAuto)) {
return;
}
Bootstrap.getEntityManagerFactoryBuilder(
this.factory.getPersistenceUnitInfo(), this.map).generateSchema();
HibernateJpaAutoConfiguration.this.applicationContext
.publishEvent(new DataSourceInitializedEvent(
HibernateJpaAutoConfiguration.this.dataSource));
}
}

View File

@ -30,6 +30,7 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitialization;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.test.City;
@ -152,6 +153,8 @@ public abstract class AbstractJpaAutoConfigurationTests {
@Test
public void usesManuallyDefinedEntityManagerFactoryBeanIfAvailable() {
EnvironmentTestUtils.addEnvironment(this.context,
"spring.datasource.initialize:false");
setupTestConfiguration(TestConfigurationWithEntityManagerFactory.class);
this.context.refresh();
LocalContainerEntityManagerFactoryBean factoryBean = this.context
@ -188,6 +191,7 @@ public abstract class AbstractJpaAutoConfigurationTests {
protected void setupTestConfiguration(Class<?> configClass) {
this.context.register(configClass, EmbeddedDataSourceConfiguration.class,
DataSourceInitialization.class,
PropertyPlaceholderAutoConfiguration.class, getAutoConfigureClass());
}

View File

@ -16,12 +16,16 @@
package org.springframework.boot.autoconfigure.orm.jpa;
import javax.sql.DataSource;
import org.junit.Test;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
/**
@ -38,6 +42,30 @@ public class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigura
return HibernateJpaAutoConfiguration.class;
}
@Test
public void testDataScriptWithDdlAuto() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context,
"spring.datasource.data:classpath:/city.sql",
"spring.datasource.schema:classpath:/ddl.sql");
setupTestConfiguration();
this.context.refresh();
assertEquals(new Integer(1),
new JdbcTemplate(this.context.getBean(DataSource.class)).queryForObject(
"SELECT COUNT(*) from CITY", Integer.class));
}
@Test
public void testDataScriptWithDeferredDdl() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context,
"spring.datasource.data:classpath:/city.sql",
"spring.datasource.deferDdl:true");
setupTestConfiguration();
this.context.refresh();
assertEquals(new Integer(1),
new JdbcTemplate(this.context.getBean(DataSource.class)).queryForObject(
"SELECT COUNT(*) from CITY", Integer.class));
}
@Test
public void testCustomNamingStrategy() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context,

View File

@ -0,0 +1 @@
INSERT INTO CITY (NAME, STATE, COUNTRY, MAP) values ('Washington', 'DC', 'US', 'Google');

View File

@ -156,7 +156,9 @@ content into your application; rather pick only the properties that you need.
# DATASOURCE ({sc-spring-boot-autoconfigure}/jdbc/DataSourceAutoConfiguration.{sc-ext}[DataSourceAutoConfiguration] & {sc-spring-boot-autoconfigure}//jdbc/AbstractDataSourceConfiguration.{sc-ext}[AbstractDataSourceConfiguration])
spring.datasource.name= # name of the data source
spring.datasource.initialize=true # populate using data.sql
spring.datasource.schema= # a schema resource reference
spring.datasource.deferDdl= # flag to indicate that schema scripts will run after the application starts (default false)
spring.datasource.schema= # a schema (DDL) script resource reference
spring.datasource.data= # a data (DML) script resource reference
spring.datasource.platform= # the platform to use in the schema resource (schema-${platform}.sql)
spring.datasource.continueOnError=false # continue even if can't be initialized
spring.datasource.separator=; # statement separator in SQL initialization scripts

View File

@ -1165,7 +1165,7 @@ and `data-${platform}.sql` files (if present), where
it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`,
`postgresql` etc.). Spring Boot enables the failfast feature of the Spring JDBC
initializer by default, so if the scripts cause exceptions the application will fail
to start.
to start. The script locations can be changed by setting `spring.datasource.schema` and `spring.datasource.data`, and neither location will be processed if `spring.datasource.initialize=false`.
To disable the failfast you can set `spring.datasource.continueOnError=true`. This can be
useful once an application has matured and been deployed a few times, since the scripts