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
This commit is contained in:
Stéphane Nicoll
2025-03-06 11:22:03 +01:00
parent d097870de5
commit e06244d007
10 changed files with 362 additions and 47 deletions

View File

@@ -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<String, Object> getVendorProperties() {
Supplier<String> defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(getDataSource());
protected Map<String, Object> getVendorProperties(DataSource dataSource) {
Supplier<String> defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(dataSource);
return new LinkedHashMap<>(this.hibernateProperties.determineHibernateProperties(
getProperties().getProperties(), new HibernateSettings().ddlAuto(defaultDdlMode)
.hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers)));

View File

@@ -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> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> 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<String, ?> buildJpaProperties() {
private Map<String, ?> buildJpaProperties(DataSource dataSource) {
Map<String, Object> properties = new HashMap<>(this.properties.getProperties());
Map<String, Object> vendorProperties = getVendorProperties();
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> getVendorProperties() {
return getVendorProperties(getDataSource());
}
/**
* Customize vendor properties before they are used. Allows for post-processing (for

View File

@@ -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<String, Object> 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<String, ?> configureJpaProperties() {
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 {

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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 = """
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="manually-configured">
<class>org.springframework.boot.autoconfigure.flyway.FlywayAutoConfigurationTests$City</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
</persistence>
""")
@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();
}
}
}

View File

@@ -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<String, Object> hibernateProperties = jpaConfiguration.getVendorProperties();
Map<String, Object> 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<String, Object> hibernateProperties = jpaConfiguration.getVendorProperties();
Map<String, Object> hibernateProperties = jpaConfiguration
.getVendorProperties(context.getBean(DataSource.class));
assertThat(hibernateProperties).containsEntry("test.counter", 2);
});
}

View File

@@ -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<String, Object> hibernateProperties = context.getBean(HibernateJpaConfiguration.class)
.getVendorProperties();
Map<String, Object> 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<String, Object> hibernateProperties = context.getBean(HibernateJpaConfiguration.class)
.getVendorProperties();
Map<String, Object> 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<String, Object> hibernateProperties = context.getBean(HibernateJpaConfiguration.class)
.getVendorProperties();
Map<String, Object> 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<String, Object> hibernateProperties = context.getBean(HibernateJpaConfiguration.class)
.getVendorProperties();
Map<String, Object> hibernateProperties = getVendorProperties(context);
TestHibernatePropertiesCustomizerConfiguration configuration = context
.getBean(TestHibernatePropertiesCustomizerConfiguration.class);
assertThat(hibernateProperties).contains(
@@ -536,8 +533,11 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
private ContextConsumer<AssertableApplicationContext> vendorProperties(
Consumer<Map<String, Object>> vendorProperties) {
return (context) -> vendorProperties
.accept(context.getBean(HibernateJpaConfiguration.class).getVendorProperties());
return (context) -> vendorProperties.accept(getVendorProperties(context));
}
private static Map<String, Object> getVendorProperties(ConfigurableApplicationContext context) {
return context.getBean(HibernateJpaConfiguration.class).getVendorProperties(context.getBean(DataSource.class));
}
@Test