From a5543f18b9a9b72ca0d7889230ad44c997289ad4 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 19 May 2014 10:50:14 +0100 Subject: [PATCH] Add callback for modifying or inspecting LocalContainerEntityManagerFactoryBean A callback is added in autoconfig, so that if users inject the EntityManagerFactoryBuilder into their app and use it to create multiple EntityManagerFactories, they all get the same deferred DDL behaviour. The deferred DDL can also be disabled by setting spring.jpa.hibernate.deferDdl=true. Fixes gh-894 --- .../orm/jpa/EntityManagerFactoryBuilder.java | 33 +++++++++- .../jpa/HibernateJpaAutoConfiguration.java | 50 ++++++++++---- .../orm/jpa/JpaBaseConfiguration.java | 3 + .../autoconfigure/orm/jpa/JpaProperties.java | 26 ++++++-- .../jpa/EntityManagerFactoryBuilderTests.java | 66 +++++++++++++++++++ .../appendix-application-properties.adoc | 3 +- .../main/asciidoc/spring-boot-features.adoc | 6 +- .../flyway/SampleFlywayApplication.java | 2 +- 8 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilderTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java index f603e42247..9b0f793732 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.orm.jpa; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -46,6 +47,8 @@ public class EntityManagerFactoryBuilder { private JpaProperties properties; + private EntityManagerFactoryBeanCallback callback; + /** * Create a new instance passing in the common pieces that will be shared if multiple * EntityManagerFactory instances are created. @@ -66,6 +69,15 @@ public class EntityManagerFactoryBuilder { return new Builder(dataSource); } + /** + * An optional callback for new entity manager factory beans. + * + * @author Dave Syer + */ + public void setCallback(EntityManagerFactoryBeanCallback callback) { + this.callback = callback; + } + /** * A fluent builder for a LocalContainerEntityManagerFactoryBean. */ @@ -77,6 +89,8 @@ public class EntityManagerFactoryBuilder { private String persistenceUnit; + private Map properties = new HashMap(); + private Builder(DataSource dataSource) { this.dataSource = dataSource; } @@ -126,8 +140,7 @@ public class EntityManagerFactoryBuilder { * @return the builder for fluent usage */ public Builder properties(Map properties) { - EntityManagerFactoryBuilder.this.properties.getProperties() - .putAll(properties); + this.properties.putAll(properties); return this; } @@ -146,9 +159,25 @@ public class EntityManagerFactoryBuilder { entityManagerFactoryBean.setPackagesToScan(this.packagesToScan); entityManagerFactoryBean.getJpaPropertyMap().putAll( EntityManagerFactoryBuilder.this.properties.getProperties()); + entityManagerFactoryBean.getJpaPropertyMap().putAll(this.properties); + if (EntityManagerFactoryBuilder.this.callback != null) { + EntityManagerFactoryBuilder.this.callback + .execute(entityManagerFactoryBean); + } return entityManagerFactoryBean; } } + /** + * A callback for new entity manager factory beans created by a Builder. + * + * @author Dave Syer + */ + public static interface EntityManagerFactoryBeanCallback { + + void execute(LocalContainerEntityManagerFactoryBean factory); + + } + } 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 253d25301f..8d3945af11 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 @@ -22,7 +22,6 @@ import javax.persistence.EntityManager; import javax.sql.DataSource; import org.hibernate.jpa.boot.spi.Bootstrap; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -30,8 +29,10 @@ 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.orm.jpa.EntityManagerFactoryBuilder.EntityManagerFactoryBeanCallback; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.HibernateEntityManagerCondition; import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -53,8 +54,7 @@ import org.springframework.util.ClassUtils; EnableTransactionManagement.class, EntityManager.class }) @Conditional(HibernateEntityManagerCondition.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) -public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration implements - ApplicationListener { +public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration { @Autowired private JpaProperties properties; @@ -63,7 +63,7 @@ public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration implemen private DataSource dataSource; @Autowired - private BeanFactory beanFactory; + private ConfigurableApplicationContext applicationContext; @Override protected AbstractJpaVendorAdapter createJpaVendorAdapter() { @@ -76,15 +76,41 @@ public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration implemen } @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - Map map = this.properties.getHibernateProperties(this.dataSource); - if ("none".equals(map.get("hibernate.hbm2ddl.auto"))) { - return; + protected EntityManagerFactoryBeanCallback getVendorCallback() { + final Map map = this.properties + .getHibernateProperties(this.dataSource); + return new EntityManagerFactoryBeanCallback() { + @Override + public void execute( + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { + HibernateJpaAutoConfiguration.this.applicationContext + .addApplicationListener(new DeferredSchemaAction( + entityManagerFactoryBean, map)); + } + }; + } + + private static class DeferredSchemaAction implements + ApplicationListener { + + private Map map; + private LocalContainerEntityManagerFactoryBean factory; + + public DeferredSchemaAction(LocalContainerEntityManagerFactoryBean factory, + Map map) { + this.factory = factory; + this.map = map; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + String ddlAuto = (String) this.map.get("hibernate.hbm2ddl.auto"); + if (ddlAuto == null || "none".equals(ddlAuto)) { + return; + } + Bootstrap.getEntityManagerFactoryBuilder( + this.factory.getPersistenceUnitInfo(), this.map).generateSchema(); } - LocalContainerEntityManagerFactoryBean factory = this.beanFactory - .getBean(LocalContainerEntityManagerFactoryBean.class); - Bootstrap.getEntityManagerFactoryBuilder(factory.getPersistenceUnitInfo(), map) - .generateSchema(); } static class HibernateEntityManagerCondition extends SpringBootCondition { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index 8b07c87e05..5fd8e3e8fb 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -90,6 +90,7 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware { JpaVendorAdapter jpaVendorAdapter) { EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder( jpaVendorAdapter, this.jpaProperties, this.persistenceUnitManager); + builder.setCallback(getVendorCallback()); return builder; } @@ -106,6 +107,8 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware { protected abstract Map getVendorProperties(); + protected abstract EntityManagerFactoryBuilder.EntityManagerFactoryBeanCallback getVendorCallback(); + protected String[] getPackagesToScan() { List basePackages = AutoConfigurationPackages.get(this.beanFactory); return basePackages.toArray(new String[basePackages.size()]); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java index cf61e897db..57efa9f3ee 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java @@ -96,7 +96,7 @@ public class JpaProperties { } /** - * Get configuration properties for the initialization of the main + * Get configuration properties for the initialization of the main Hibernate * EntityManagerFactory. The result will always have ddl-auto=none, so that the schema * generation or validation can be deferred to a later stage. * @@ -108,7 +108,7 @@ public class JpaProperties { } /** - * Get the full configuration properties the Hibernate EntityManagerFactory. + * Get the full configuration properties for the Hibernate EntityManagerFactory. * * @param dataSource the DataSource in case it is needed to determine the properties * @return some Hibernate properties for configuration @@ -126,6 +126,8 @@ public class JpaProperties { private String ddlAuto; + private boolean deferDdl = true; + public Class getNamingStrategy() { return this.namingStrategy; } @@ -135,11 +137,22 @@ public class JpaProperties { } public String getDdlAuto() { - return "none"; + return this.ddlAuto; + } + + public void setDeferDdl(boolean deferDdl) { + this.deferDdl = deferDdl; + } + + public boolean isDeferDdl() { + return this.deferDdl; } private String getDeferredDdlAuto(Map existing, DataSource dataSource) { + if (!this.deferDdl) { + return "none"; + } String ddlAuto = this.ddlAuto != null ? this.ddlAuto : getDefaultDdlAuto(dataSource); if (!isAlreadyProvided(existing, "hbm2ddl.auto") && !"none".equals(ddlAuto)) { @@ -173,7 +186,12 @@ public class JpaProperties { result.put("hibernate.ejb.naming_strategy", DEFAULT_NAMING_STRATEGY.getName()); } - result.put("hibernate.hbm2ddl.auto", "none"); + if (this.deferDdl) { + result.put("hibernate.hbm2ddl.auto", "none"); + } + else { + result.put("hibernate.hbm2ddl.auto", this.ddlAuto); + } return result; } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilderTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilderTests.java new file mode 100644 index 0000000000..1623978427 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilderTests.java @@ -0,0 +1,66 @@ +/* + * 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.orm.jpa; + +import java.util.Collections; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Dave Syer + */ +public class EntityManagerFactoryBuilderTests { + + private JpaProperties properties = new JpaProperties(); + + private DataSource dataSource1 = Mockito.mock(DataSource.class); + + private DataSource dataSource2 = Mockito.mock(DataSource.class); + + @Test + public void entityManagerFactoryPropertiesNotOverwritingDefaults() { + EntityManagerFactoryBuilder factory = new EntityManagerFactoryBuilder( + new HibernateJpaVendorAdapter(), this.properties, null); + LocalContainerEntityManagerFactoryBean result1 = factory + .dataSource(this.dataSource1) + .properties(Collections.singletonMap("foo", (Object) "spam")).build(); + assertFalse(result1.getJpaPropertyMap().isEmpty()); + assertTrue(this.properties.getProperties().isEmpty()); + } + + @Test + public void multipleEntityManagerFactoriesDoNotOverwriteEachOther() { + EntityManagerFactoryBuilder factory = new EntityManagerFactoryBuilder( + new HibernateJpaVendorAdapter(), this.properties, null); + LocalContainerEntityManagerFactoryBean result1 = factory + .dataSource(this.dataSource1) + .properties(Collections.singletonMap("foo", (Object) "spam")).build(); + assertFalse(result1.getJpaPropertyMap().isEmpty()); + LocalContainerEntityManagerFactoryBean result2 = factory.dataSource( + this.dataSource2).build(); + assertTrue(result2.getJpaPropertyMap().isEmpty()); + } + +} 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 d6d6d428b5..dc753604b9 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -167,8 +167,9 @@ content into your application; rather pick only the properties that you need. spring.jpa.show-sql=true spring.jpa.database-platform= spring.jpa.database= - spring.jpa.generate-ddl= + spring.jpa.generate-ddl=false # ignored by Hibernate, might be useful for other vendors spring.jpa.hibernate.naming-strategy= # naming classname + spring.jpa.hibernate.defer-ddl=true # defer processing of DDL until application is running spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs # FLYWAY ({sc-spring-boot-autoconfigure}/flyway/FlywayProperties.{sc-ext}[FlywayProperties]) diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 66037d0304..a10fd3b6de 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1397,8 +1397,10 @@ following to your `application.properties`. NOTE: Hibernate's own internal property name for this (if you happen to remember it better) is `hibernate.hbm2ddl.auto`. You can set it, along with other Hibernate native properties, using `spring.jpa.properties.*` (the prefix is stripped before adding them -to the entity manager). Also, `spring.jpa.generate-ddl=false` switches off all -DDL generation. +to the entity manager). By default the DDL execution (or validation) is deferred until +the `ApplicationContext` has started. There is also a `spring.jpa.generate-ddl` flag, but +it is not used if Hibernate autoconfig is active because the `ddl-auto` +settings are more fine grained. diff --git a/spring-boot-samples/spring-boot-sample-flyway/src/main/java/sample/flyway/SampleFlywayApplication.java b/spring-boot-samples/spring-boot-sample-flyway/src/main/java/sample/flyway/SampleFlywayApplication.java index 0f89083ecc..71bb8c9e74 100644 --- a/spring-boot-samples/spring-boot-sample-flyway/src/main/java/sample/flyway/SampleFlywayApplication.java +++ b/spring-boot-samples/spring-boot-sample-flyway/src/main/java/sample/flyway/SampleFlywayApplication.java @@ -78,7 +78,7 @@ class Person { @Override public String toString() { - return "Person [firstName=" + firstName + ", lastname=" + lastName + return "Person [firstName=" + firstName + ", lastName=" + lastName + "]"; } }