From e06244d0075d1d80acd19d1d6df66f3b50bd3d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 6 Mar 2025 11:22:03 +0100 Subject: [PATCH] Adapt EntityManagerFactoryBuilder to work with multiple data sources This commit allows EntityManagerFactoryBuilder to provide the JPA properties to use according to the DataSource used to build the EntityManagerFactory. Previously the JPA properties were computed only once based on the primary data source, which was a problem since its default DDL setting may be different. EntityManagerFactoryBuilder takes a function that provides the JPA properties based on a data source, rather than the properties themselves. Constructors with the previous variant have been deprecated as a result. Closes gh-44516 --- ...ibernateMetricsAutoConfigurationTests.java | 9 +- .../orm/jpa/HibernateJpaConfiguration.java | 6 +- .../orm/jpa/JpaBaseConfiguration.java | 29 +++- .../flyway/FlywayAutoConfigurationTests.java | 92 ++++++++++- .../LiquibaseAutoConfigurationTests.java | 153 ++++++++++++++++++ ...tomHibernateJpaAutoConfigurationTests.java | 8 +- .../HibernateJpaAutoConfigurationTests.java | 20 +-- ...onalEntityManagerFactoryConfiguration.java | 16 +- ...tionalEntityManagerFactoryConfiguration.kt | 13 +- .../orm/jpa/EntityManagerFactoryBuilder.java | 63 +++++++- 10 files changed, 362 insertions(+), 47 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java index 651cfead8b0..fff5efec038 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java @@ -16,8 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa; -import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import javax.sql.DataSource; @@ -194,9 +194,10 @@ class HibernateMetricsAutoConfigurationTests { } private LocalContainerEntityManagerFactoryBean createSessionFactory(DataSource ds) { - Map jpaProperties = new HashMap<>(); - jpaProperties.put("hibernate.generate_statistics", "true"); - return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), jpaProperties, null).dataSource(ds) + Function> jpaPropertiesFactory = (dataSource) -> Map + .of("hibernate.generate_statistics", "true"); + return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), jpaPropertiesFactory, null) + .dataSource(ds) .packages(PACKAGE_CLASSES) .build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java index fe8b19db079..8c9144b85ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,8 +138,8 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { } @Override - protected Map getVendorProperties() { - Supplier defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(getDataSource()); + protected Map getVendorProperties(DataSource dataSource) { + Supplier defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(dataSource); return new LinkedHashMap<>(this.hibernateProperties.determineHibernateProperties( getProperties().getProperties(), new HibernateSettings().ddlAuto(defaultDdlMode) .hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers))); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index bf6a6ceff76..80306aaf906 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,15 +120,15 @@ public abstract class JpaBaseConfiguration { public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, ObjectProvider persistenceUnitManager, ObjectProvider customizers) { - EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, buildJpaProperties(), - persistenceUnitManager.getIfAvailable()); + EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, + this::buildJpaProperties, persistenceUnitManager.getIfAvailable()); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } - private Map buildJpaProperties() { + private Map buildJpaProperties(DataSource dataSource) { Map properties = new HashMap<>(this.properties.getProperties()); - Map vendorProperties = getVendorProperties(); + Map vendorProperties = getVendorProperties(dataSource); customizeVendorProperties(vendorProperties); properties.putAll(vendorProperties); return properties; @@ -148,7 +148,24 @@ public abstract class JpaBaseConfiguration { protected abstract AbstractJpaVendorAdapter createJpaVendorAdapter(); - protected abstract Map getVendorProperties(); + /** + * Return the vendor-specific properties for the given {@link DataSource}. + * @param dataSource the data source + * @return the vendor properties + * @since 3.4.4 + */ + protected abstract Map getVendorProperties(DataSource dataSource); + + /** + * Return the vendor-specific properties. + * @return the vendor properties + * @deprecated since 3.4.4 for removal in 3.6.0 in favor of + * {@link #getVendorProperties(DataSource)} + */ + @Deprecated(since = "3.4.4", forRemoval = true) + protected Map getVendorProperties() { + return getVendorProperties(getDataSource()); + } /** * Customize vendor properties before they are used. Allows for post-processing (for diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index f9e049d8dfd..c695b70a98d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -65,11 +65,14 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.Fly import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.OracleFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.PostgresqlFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.SqlServerFlywayConfigurationCustomizer; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.jdbc.SchemaManagement; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.test.context.FilteredClassLoader; @@ -489,6 +492,36 @@ class FlywayAutoConfigurationTests { .run((context) -> assertThat(context).hasNotFailed()); } + @Test + @WithMetaInfPersistenceXmlResource + void jpaApplyDdl() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class)) + .run((context) -> { + Map jpaProperties = context.getBean(LocalContainerEntityManagerFactoryBean.class) + .getJpaPropertyMap(); + assertThat(jpaProperties).doesNotContainKey("hibernate.hbm2ddl.auto"); + }); + } + + @Test + @WithMetaInfPersistenceXmlResource + void jpaAndMultipleDataSourcesApplyDdl() { + this.contextRunner.withConfiguration(AutoConfigurations.of(HibernateJpaAutoConfiguration.class)) + .withUserConfiguration(JpaWithMultipleDataSourcesConfiguration.class) + .run((context) -> { + LocalContainerEntityManagerFactoryBean normalEntityManagerFactoryBean = context + .getBean("&normalEntityManagerFactory", LocalContainerEntityManagerFactoryBean.class); + assertThat(normalEntityManagerFactoryBean.getJpaPropertyMap()).containsEntry("configured", "normal") + .containsEntry("hibernate.hbm2ddl.auto", "create-drop"); + LocalContainerEntityManagerFactoryBean flywayEntityManagerFactoryBean = context + .getBean("&flywayEntityManagerFactory", LocalContainerEntityManagerFactoryBean.class); + assertThat(flywayEntityManagerFactoryBean.getJpaPropertyMap()).containsEntry("configured", "flyway") + .doesNotContainKey("hibernate.hbm2ddl.auto"); + }); + } + @Test void customFlywayWithJdbc() { this.contextRunner @@ -962,6 +995,13 @@ class FlywayAutoConfigurationTests { }; } + private static Map configureJpaProperties() { + Map properties = new HashMap<>(); + properties.put("configured", "manually"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + return properties; + } + @Configuration(proxyBeanMethods = false) static class FlywayDataSourceConfiguration { @@ -1057,10 +1097,8 @@ class FlywayAutoConfigurationTests { @Bean LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource) { - Map properties = new HashMap<>(); - properties.put("configured", "manually"); - properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); - return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), properties, null) + return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), (ds) -> configureJpaProperties(), + null) .dataSource(dataSource) .build(); } @@ -1083,16 +1121,54 @@ class FlywayAutoConfigurationTests { @Bean LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() { - Map properties = new HashMap<>(); - properties.put("configured", "manually"); - properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); - return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), properties, null) + return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), + (datasource) -> configureJpaProperties(), null) .dataSource(this.dataSource) .build(); } } + @Configuration(proxyBeanMethods = false) + static class JpaWithMultipleDataSourcesConfiguration { + + @Bean + @Primary + DataSource normalDataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseConnection.HSQLDB.getType()) + .generateUniqueName(true) + .build(); + } + + @Bean + @Primary + LocalContainerEntityManagerFactoryBean normalEntityManagerFactory(EntityManagerFactoryBuilder builder, + DataSource normalDataSource) { + Map properties = new HashMap<>(); + properties.put("configured", "normal"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + return builder.dataSource(normalDataSource).properties(properties).build(); + } + + @Bean + @FlywayDataSource + DataSource flywayDataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseConnection.HSQLDB.getType()) + .generateUniqueName(true) + .build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean flywayEntityManagerFactory(EntityManagerFactoryBuilder builder, + @FlywayDataSource DataSource flywayDataSource) { + Map properties = new HashMap<>(); + properties.put("configured", "flyway"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + return builder.dataSource(flywayDataSource).properties(properties).build(); + } + + } + @Configuration static class CustomFlywayWithJdbcConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 8a7869dc530..798ebf0067e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.liquibase; import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -26,6 +27,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; +import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.function.Consumer; @@ -33,6 +35,10 @@ import java.util.function.Consumer; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; import liquibase.Liquibase; import liquibase.UpdateSummaryEnum; import liquibase.UpdateSummaryOutputEnum; @@ -40,6 +46,7 @@ import liquibase.command.core.helpers.ShowSummaryArgument; import liquibase.integration.spring.Customizer; import liquibase.integration.spring.SpringLiquibase; import liquibase.ui.UIServiceEnum; +import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -49,6 +56,7 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; @@ -56,6 +64,8 @@ import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -71,6 +81,7 @@ import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -549,6 +560,38 @@ class LiquibaseAutoConfigurationTests { }); } + @Test + @WithDbChangelogMasterYamlResource + @WithMetaInfPersistenceXmlResource + void jpaApplyDdl() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class)) + .run((context) -> { + Map jpaProperties = context.getBean(LocalContainerEntityManagerFactoryBean.class) + .getJpaPropertyMap(); + assertThat(jpaProperties).doesNotContainKey("hibernate.hbm2ddl.auto"); + }); + } + + @Test + @WithDbChangelogMasterYamlResource + @WithMetaInfPersistenceXmlResource + void jpaAndMultipleDataSourcesApplyDdl() { + this.contextRunner.withConfiguration(AutoConfigurations.of(HibernateJpaAutoConfiguration.class)) + .withUserConfiguration(JpaWithMultipleDataSourcesConfiguration.class) + .run((context) -> { + LocalContainerEntityManagerFactoryBean normalEntityManagerFactoryBean = context + .getBean("&normalEntityManagerFactory", LocalContainerEntityManagerFactoryBean.class); + assertThat(normalEntityManagerFactoryBean.getJpaPropertyMap()).containsEntry("configured", "normal") + .containsEntry("hibernate.hbm2ddl.auto", "create-drop"); + LocalContainerEntityManagerFactoryBean liquibaseEntityManagerFactory = context + .getBean("&liquibaseEntityManagerFactory", LocalContainerEntityManagerFactoryBean.class); + assertThat(liquibaseEntityManagerFactory.getJpaPropertyMap()).containsEntry("configured", "liquibase") + .doesNotContainKey("hibernate.hbm2ddl.auto"); + }); + } + @Test @WithDbChangelogMasterYamlResource void userConfigurationJdbcTemplateDependency() { @@ -649,6 +692,46 @@ class LiquibaseAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JpaWithMultipleDataSourcesConfiguration { + + @Bean + @Primary + DataSource normalDataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseConnection.HSQLDB.getType()) + .generateUniqueName(true) + .build(); + } + + @Bean + @Primary + LocalContainerEntityManagerFactoryBean normalEntityManagerFactory(EntityManagerFactoryBuilder builder, + DataSource normalDataSource) { + Map properties = new HashMap<>(); + properties.put("configured", "normal"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + return builder.dataSource(normalDataSource).properties(properties).build(); + } + + @Bean + @LiquibaseDataSource + DataSource liquibaseDataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseConnection.HSQLDB.getType()) + .generateUniqueName(true) + .build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean liquibaseEntityManagerFactory(EntityManagerFactoryBuilder builder, + @LiquibaseDataSource DataSource liquibaseDataSource) { + Map properties = new HashMap<>(); + properties.put("configured", "liquibase"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + return builder.dataSource(liquibaseDataSource).properties(properties).build(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomDataSourceConfiguration { @@ -785,4 +868,74 @@ class LiquibaseAutoConfigurationTests { } + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @WithResource(name = "META-INF/persistence.xml", + content = """ + + + + org.springframework.boot.autoconfigure.flyway.FlywayAutoConfigurationTests$City + true + + + """) + @interface WithMetaInfPersistenceXmlResource { + + } + + @Entity + public static class City implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String state; + + @Column(nullable = false) + private String country; + + @Column(nullable = false) + private String map; + + protected City() { + } + + City(String name, String state, String country, String map) { + this.name = name; + this.state = state; + this.country = country; + this.map = map; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + public String getCountry() { + return this.country; + } + + public String getMap() { + return this.map; + } + + @Override + public String toString() { + return getName() + "," + getState() + "," + getCountry(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java index 7d5fe8cdd7f..64c538416a6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,8 @@ class CustomHibernateJpaAutoConfigurationTests { .withPropertyValues("spring.datasource.url:jdbc:h2:mem:naming-strategy-beans") .run((context) -> { HibernateJpaConfiguration jpaConfiguration = context.getBean(HibernateJpaConfiguration.class); - Map hibernateProperties = jpaConfiguration.getVendorProperties(); + Map hibernateProperties = jpaConfiguration + .getVendorProperties(context.getBean(DataSource.class)); assertThat(hibernateProperties).containsEntry("hibernate.implicit_naming_strategy", NamingStrategyConfiguration.implicitNamingStrategy); assertThat(hibernateProperties).containsEntry("hibernate.physical_naming_strategy", @@ -93,7 +94,8 @@ class CustomHibernateJpaAutoConfigurationTests { void hibernatePropertiesCustomizersAreAppliedInOrder() { this.contextRunner.withUserConfiguration(HibernatePropertiesCustomizerConfiguration.class).run((context) -> { HibernateJpaConfiguration jpaConfiguration = context.getBean(HibernateJpaConfiguration.class); - Map hibernateProperties = jpaConfiguration.getVendorProperties(); + Map hibernateProperties = jpaConfiguration + .getVendorProperties(context.getBean(DataSource.class)); assertThat(hibernateProperties).containsEntry("test.counter", 2); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index e5d5b141dfa..f25bfc362d4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -75,6 +75,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.ContextRefreshedEvent; @@ -399,8 +400,7 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes @Test void physicalNamingStrategyCanBeUsed() { contextRunner().withUserConfiguration(TestPhysicalNamingStrategyConfiguration.class).run((context) -> { - Map hibernateProperties = context.getBean(HibernateJpaConfiguration.class) - .getVendorProperties(); + Map hibernateProperties = getVendorProperties(context); assertThat(hibernateProperties) .contains(entry("hibernate.physical_naming_strategy", context.getBean("testPhysicalNamingStrategy"))); assertThat(hibernateProperties).doesNotContainKeys("hibernate.ejb.naming_strategy"); @@ -410,8 +410,7 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes @Test void implicitNamingStrategyCanBeUsed() { contextRunner().withUserConfiguration(TestImplicitNamingStrategyConfiguration.class).run((context) -> { - Map hibernateProperties = context.getBean(HibernateJpaConfiguration.class) - .getVendorProperties(); + Map hibernateProperties = getVendorProperties(context); assertThat(hibernateProperties) .contains(entry("hibernate.implicit_naming_strategy", context.getBean("testImplicitNamingStrategy"))); assertThat(hibernateProperties).doesNotContainKeys("hibernate.ejb.naming_strategy"); @@ -426,8 +425,7 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes .withPropertyValues("spring.jpa.hibernate.naming.physical-strategy:com.example.Physical", "spring.jpa.hibernate.naming.implicit-strategy:com.example.Implicit") .run((context) -> { - Map hibernateProperties = context.getBean(HibernateJpaConfiguration.class) - .getVendorProperties(); + Map hibernateProperties = getVendorProperties(context); assertThat(hibernateProperties).contains( entry("hibernate.physical_naming_strategy", context.getBean("testPhysicalNamingStrategy")), entry("hibernate.implicit_naming_strategy", context.getBean("testImplicitNamingStrategy"))); @@ -443,8 +441,7 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes .withPropertyValues("spring.jpa.hibernate.naming.physical-strategy:com.example.Physical", "spring.jpa.hibernate.naming.implicit-strategy:com.example.Implicit") .run((context) -> { - Map hibernateProperties = context.getBean(HibernateJpaConfiguration.class) - .getVendorProperties(); + Map hibernateProperties = getVendorProperties(context); TestHibernatePropertiesCustomizerConfiguration configuration = context .getBean(TestHibernatePropertiesCustomizerConfiguration.class); assertThat(hibernateProperties).contains( @@ -536,8 +533,11 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes private ContextConsumer vendorProperties( Consumer> vendorProperties) { - return (context) -> vendorProperties - .accept(context.getBean(HibernateJpaConfiguration.class).getVendorProperties()); + return (context) -> vendorProperties.accept(getVendorProperties(context)); + } + + private static Map getVendorProperties(ConfigurableApplicationContext context) { + return context.getBean(HibernateJpaConfiguration.class).getVendorProperties(context.getBean(DataSource.class)); } @Test diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.java index 8d26116a735..76a05743a0c 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; @@ -48,7 +52,9 @@ public class MyAdditionalEntityManagerFactoryConfiguration { private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) { JpaVendorAdapter jpaVendorAdapter = createJpaVendorAdapter(jpaProperties); - return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaProperties.getProperties(), null); + Function> jpaPropertiesFactory = (dataSource) -> createJpaProperties(dataSource, + jpaProperties.getProperties()); + return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaPropertiesFactory, null); } private JpaVendorAdapter createJpaVendorAdapter(JpaProperties jpaProperties) { @@ -56,4 +62,10 @@ public class MyAdditionalEntityManagerFactoryConfiguration { return new HibernateJpaVendorAdapter(); } + private Map createJpaProperties(DataSource dataSource, Map existingProperties) { + Map jpaProperties = new LinkedHashMap<>(existingProperties); + // ... map JPA properties that require the DataSource (e.g. DDL flags) + return jpaProperties; + } + } diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.kt index 55eefdd7e58..4b57a239fde 100644 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.kt +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyAdditionalEntityManagerFactoryConfiguration.kt @@ -16,8 +16,6 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers -import javax.sql.DataSource - import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties import org.springframework.boot.context.properties.ConfigurationProperties @@ -27,6 +25,7 @@ import org.springframework.context.annotation.Configuration import org.springframework.orm.jpa.JpaVendorAdapter import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter +import javax.sql.DataSource @Suppress("UNUSED_PARAMETER") @Configuration(proxyBeanMethods = false) @@ -51,7 +50,9 @@ class MyAdditionalEntityManagerFactoryConfiguration { private fun createEntityManagerFactoryBuilder(jpaProperties: JpaProperties): EntityManagerFactoryBuilder { val jpaVendorAdapter = createJpaVendorAdapter(jpaProperties) - return EntityManagerFactoryBuilder(jpaVendorAdapter, jpaProperties.properties, null) + val jpaPropertiesFactory = { dataSource: DataSource -> + createJpaProperties(dataSource, jpaProperties.properties) } + return EntityManagerFactoryBuilder(jpaVendorAdapter, jpaPropertiesFactory, null) } private fun createJpaVendorAdapter(jpaProperties: JpaProperties): JpaVendorAdapter { @@ -59,4 +60,10 @@ class MyAdditionalEntityManagerFactoryConfiguration { return HibernateJpaVendorAdapter() } + private fun createJpaProperties(dataSource: DataSource, existingProperties: Map): Map { + val jpaProperties: Map = LinkedHashMap(existingProperties) + // ... map JPA properties that require the DataSource (e.g. DDL flags) + return jpaProperties + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityManagerFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityManagerFactoryBuilder.java index 8091f99b06a..15bbcea920c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityManagerFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityManagerFactoryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import java.util.function.Function; import javax.sql.DataSource; @@ -54,7 +55,7 @@ public class EntityManagerFactoryBuilder { private final PersistenceUnitManager persistenceUnitManager; - private final Map jpaProperties; + private final Function> jpaPropertiesFactory; private final URL persistenceUnitRootLocation; @@ -62,6 +63,42 @@ public class EntityManagerFactoryBuilder { private PersistenceUnitPostProcessor[] persistenceUnitPostProcessors; + /** + * Create a new instance passing in the common pieces that will be shared if multiple + * EntityManagerFactory instances are created. + * @param jpaVendorAdapter a vendor adapter + * @param jpaPropertiesFactory the JPA properties to be passed to the persistence + * provider, based on the {@linkplain #dataSource(DataSource) configured data source} + * @param persistenceUnitManager optional source of persistence unit information (can + * be null) + * @since 3.4.4 + */ + public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, + Function> jpaPropertiesFactory, PersistenceUnitManager persistenceUnitManager) { + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, null); + } + + /** + * Create a new instance passing in the common pieces that will be shared if multiple + * EntityManagerFactory instances are created. + * @param jpaVendorAdapter a vendor adapter + * @param jpaPropertiesFactory the JPA properties to be passed to the persistence + * provider, based on the {@linkplain #dataSource(DataSource) configured data source} + * @param persistenceUnitManager optional source of persistence unit information (can + * be null) + * @param persistenceUnitRootLocation the persistence unit root location to use as a + * fallback or {@code null} + * @since 3.4.4 + */ + public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, + Function> jpaPropertiesFactory, PersistenceUnitManager persistenceUnitManager, + URL persistenceUnitRootLocation) { + this.jpaVendorAdapter = jpaVendorAdapter; + this.persistenceUnitManager = persistenceUnitManager; + this.jpaPropertiesFactory = jpaPropertiesFactory; + this.persistenceUnitRootLocation = persistenceUnitRootLocation; + } + /** * Create a new instance passing in the common pieces that will be shared if multiple * EntityManagerFactory instances are created. @@ -69,10 +106,13 @@ public class EntityManagerFactoryBuilder { * @param jpaProperties the JPA properties to be passed to the persistence provider * @param persistenceUnitManager optional source of persistence unit information (can * be null) + * @deprecated since 3.4.4 for removal in 3.6.0 in favor of + * {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager)} */ + @Deprecated(since = "3.4.4", forRemoval = true) public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Map jpaProperties, PersistenceUnitManager persistenceUnitManager) { - this(jpaVendorAdapter, jpaProperties, persistenceUnitManager, null); + this(jpaVendorAdapter, (datasource) -> jpaProperties, persistenceUnitManager, null); } /** @@ -85,15 +125,21 @@ public class EntityManagerFactoryBuilder { * @param persistenceUnitRootLocation the persistence unit root location to use as a * fallback or {@code null} * @since 1.4.1 + * @deprecated since 3.4.4 for removal in 3.6.0 in favor of + * {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager, URL)} */ + @Deprecated(since = "3.4.4", forRemoval = true) public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Map jpaProperties, PersistenceUnitManager persistenceUnitManager, URL persistenceUnitRootLocation) { - this.jpaVendorAdapter = jpaVendorAdapter; - this.persistenceUnitManager = persistenceUnitManager; - this.jpaProperties = new LinkedHashMap<>(jpaProperties); - this.persistenceUnitRootLocation = persistenceUnitRootLocation; + this(jpaVendorAdapter, (datasource) -> jpaProperties, persistenceUnitManager, persistenceUnitRootLocation); } + /** + * Create a new {@link Builder} for a {@code EntityManagerFactory} using the settings + * of the given instance, and the given {@link DataSource}. + * @param dataSource the data source to use + * @return a builder to create an {@code EntityManagerFactory} + */ public Builder dataSource(DataSource dataSource) { return new Builder(dataSource); } @@ -255,7 +301,8 @@ public class EntityManagerFactoryBuilder { else { entityManagerFactoryBean.setPackagesToScan(this.packagesToScan); } - entityManagerFactoryBean.getJpaPropertyMap().putAll(EntityManagerFactoryBuilder.this.jpaProperties); + Map jpaProperties = EntityManagerFactoryBuilder.this.jpaPropertiesFactory.apply(this.dataSource); + entityManagerFactoryBean.getJpaPropertyMap().putAll(new LinkedHashMap<>(jpaProperties)); entityManagerFactoryBean.getJpaPropertyMap().putAll(this.properties); if (!ObjectUtils.isEmpty(this.mappingResources)) { entityManagerFactoryBean.setMappingResources(this.mappingResources);