From 49a09c807c437ad9b668652a89ea70c122aff89a Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 2 Jun 2014 13:07:56 +0100 Subject: [PATCH] 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 --- .../jdbc/DataSourceAutoConfiguration.java | 70 +------ .../jdbc/DataSourceInitialization.java | 181 ++++++++++++++++++ .../jdbc/DataSourceProperties.java | 20 ++ .../jpa/HibernateJpaAutoConfiguration.java | 8 +- .../AbstractJpaAutoConfigurationTests.java | 4 + .../HibernateJpaAutoConfigurationTests.java | 28 +++ .../src/test/resources/city.sql | 1 + .../appendix-application-properties.adoc | 4 +- spring-boot-docs/src/main/asciidoc/howto.adoc | 2 +- 9 files changed, 245 insertions(+), 73 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java create mode 100644 spring-boot-autoconfigure/src/test/resources/city.sql diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index e1edc77fd6..992dbea54b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -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 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 getSchemaResources(String schema) { - List resources = new ArrayList(); - 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}. diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java new file mode 100644 index 0000000000..3d7bd07314 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java @@ -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 { + + 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 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 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 getSchemaResources(String schema) { + List resources = new ArrayList(); + 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 { + + @Override + public void onApplicationEvent(DataSourceInitializedEvent event) { + runDataScripts(); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java index 830f751b60..2f81c58454 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java @@ -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; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index 28157dab33..a9e998f685 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -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 { private Map 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)); } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java index 7dc05fe762..2e6ec867e6 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java @@ -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()); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 41f4c12477..a4561af517 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -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, diff --git a/spring-boot-autoconfigure/src/test/resources/city.sql b/spring-boot-autoconfigure/src/test/resources/city.sql new file mode 100644 index 0000000000..bb2a2eeac2 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/city.sql @@ -0,0 +1 @@ +INSERT INTO CITY (NAME, STATE, COUNTRY, MAP) values ('Washington', 'DC', 'US', 'Google'); \ No newline at end of file diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 37d748fc54..b4b37c14df 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -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 diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 647975187a..6108c14529 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -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