factory, O
* @param repositoryContext repository context providing classes.
*/
public static AotEntityManagerFactoryCreator from(AotRepositoryContext repositoryContext) {
+ return from(repositoryContext, Map.of());
+ }
+
+ /**
+ * Create a {@code PersistenceUnitContext} from the given {@link AotRepositoryContext} using Jakarta
+ * Persistence-annotated classes.
+ *
+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information.
+ *
+ * @param repositoryContext repository context providing classes.
+ * @param jpaProperties JPA properties to apply.
+ * @since 4.0.1
+ */
+ public static AotEntityManagerFactoryCreator from(AotRepositoryContext repositoryContext,
+ Map jpaProperties) {
List typeNames = repositoryContext.getResolvedTypes().stream()
.filter(AotEntityManagerFactoryCreator::isJakartaAnnotated).map(Class::getName).toList();
- return from(PersistenceManagedTypes.of(typeNames, List.of()), typeNames);
+ return from(PersistenceManagedTypes.of(typeNames, List.of()), typeNames, jpaProperties);
}
/**
@@ -75,7 +91,21 @@ public static AotEntityManagerFactoryCreator from(AotRepositoryContext repositor
* @param persistenceUnitInfo persistence unit info to use.
*/
public static AotEntityManagerFactoryCreator from(PersistenceUnitInfo persistenceUnitInfo) {
- return from(() -> new AotMetamodel(persistenceUnitInfo), persistenceUnitInfo);
+ return from(persistenceUnitInfo, Map.of());
+ }
+
+ /**
+ * Create a {@code PersistenceUnitContext} from the given {@link PersistenceUnitInfo}.
+ *
+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information.
+ *
+ * @param persistenceUnitInfo persistence unit info to use.
+ * @param jpaProperties JPA properties to apply.
+ * @since 4.0.1
+ */
+ public static AotEntityManagerFactoryCreator from(PersistenceUnitInfo persistenceUnitInfo,
+ Map jpaProperties) {
+ return from(() -> new AotMetamodel(persistenceUnitInfo, jpaProperties), persistenceUnitInfo);
}
/**
@@ -86,11 +116,26 @@ public static AotEntityManagerFactoryCreator from(PersistenceUnitInfo persistenc
* @param managedTypes managed types to use.
*/
public static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes) {
- return from(managedTypes, managedTypes);
+ return from(managedTypes, managedTypes, Map.of());
+ }
+
+ /**
+ * Create a {@code PersistenceUnitContext} from the given {@link PersistenceManagedTypes}.
+ *
+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information.
+ *
+ * @param managedTypes managed types to use.
+ * @param jpaProperties JPA properties to apply.
+ * @since 4.0.1
+ */
+ public static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes,
+ Map jpaProperties) {
+ return from(managedTypes, managedTypes, jpaProperties);
}
- private static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes, Object cacheKey) {
- return from(() -> new AotMetamodel(managedTypes), cacheKey);
+ private static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes, Object cacheKey,
+ Map jpaProperties) {
+ return from(() -> new AotMetamodel(managedTypes, jpaProperties), cacheKey);
}
/**
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java
index 751bf19d3a..ff1e3307a6 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java
@@ -43,10 +43,16 @@
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl;
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor;
import org.hibernate.query.common.TemporalUnit;
+import org.hibernate.sql.ast.SqlAstTranslatorFactory;
+import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
+import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.data.util.Lazy;
import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo;
+import org.springframework.util.CollectionUtils;
/**
* AOT metamodel implementation that uses Hibernate to build the metamodel.
@@ -58,14 +64,26 @@
*/
class AotMetamodel implements Metamodel {
+ private static final Logger log = LoggerFactory.getLogger(AotMetamodel.class);
+
+ /**
+ * Collection of know properties causing problems during AOT if set differntly
+ */
+ private static final Map FAILSAFE_AOT_PROPERTIES = Map.of( //
+ JdbcSettings.ALLOW_METADATA_ON_BOOT, false, //
+ JdbcSettings.CONNECTION_PROVIDER, NoOpConnectionProvider.INSTANCE, //
+ QuerySettings.QUERY_STARTUP_CHECKING, false, //
+ PersistenceSettings.JPA_CALLBACKS_ENABLED, false //
+ );
private final Lazy entityManagerFactory;
private final Lazy entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager());
- public AotMetamodel(PersistenceManagedTypes managedTypes) {
- this(managedTypes.getManagedClassNames(), managedTypes.getPersistenceUnitRootUrl());
+ public AotMetamodel(PersistenceManagedTypes managedTypes, Map jpaProperties) {
+ this(managedTypes.getManagedClassNames(), managedTypes.getPersistenceUnitRootUrl(), jpaProperties);
}
- public AotMetamodel(Collection managedTypes, @Nullable URL persistenceUnitRootUrl) {
+ public AotMetamodel(Collection managedTypes, @Nullable URL persistenceUnitRootUrl,
+ Map jpaProperties) {
SpringPersistenceUnitInfo persistenceUnitInfo = new SpringPersistenceUnitInfo(
managedTypes.getClass().getClassLoader());
@@ -78,22 +96,46 @@ public AotMetamodel(Collection managedTypes, @Nullable URL persistenceUn
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
return new PersistenceUnitInfoDescriptor(persistenceUnitInfo.asStandardPersistenceUnitInfo());
- });
+ }, jpaProperties);
+ }
+
+ public AotMetamodel(PersistenceUnitInfo unitInfo, Map jpaProperties) {
+ this.entityManagerFactory = init(() -> new PersistenceUnitInfoDescriptor(unitInfo), jpaProperties);
+ }
+
+ static Lazy init(Supplier unitInfo,
+ Map jpaProperties) {
+ return Lazy.of(() -> new EntityManagerFactoryBuilderImpl(unitInfo.get(), initProperties(jpaProperties)).build());
}
- public AotMetamodel(PersistenceUnitInfo unitInfo) {
- this.entityManagerFactory = init(() -> new PersistenceUnitInfoDescriptor(unitInfo));
+ static Map initProperties(Map jpaProperties) {
+
+ Map properties = CollectionUtils
+ .newLinkedHashMap(jpaProperties.size() + FAILSAFE_AOT_PROPERTIES.size() + 1);
+
+ // we allow explicit Dialect Overrides, but put in a default one to avoid potential db access
+ properties.put(JdbcSettings.DIALECT, SpringDataJpaAotDialect.INSTANCE);
+
+ // apply user defined properties
+ properties.putAll(jpaProperties);
+
+ // override properties known to cause trouble
+ applyPropertyOverrides(properties);
+
+ return properties;
}
- static Lazy init(Supplier unitInfo) {
+ private static void applyPropertyOverrides(Map properties) {
+
+ for (Map.Entry entry : FAILSAFE_AOT_PROPERTIES.entrySet()) {
- return Lazy.of(() -> new EntityManagerFactoryBuilderImpl(unitInfo.get(),
- Map.of(JdbcSettings.DIALECT, SpringDataJpaAotDialect.INSTANCE, //
- JdbcSettings.ALLOW_METADATA_ON_BOOT, false, //
- JdbcSettings.CONNECTION_PROVIDER, new UserSuppliedConnectionProviderImpl(), //
- QuerySettings.QUERY_STARTUP_CHECKING, false, //
- PersistenceSettings.JPA_CALLBACKS_ENABLED, false))
- .build());
+ if (log.isDebugEnabled() && properties.containsKey(entry.getKey())) {
+ log.debug("Overriding property [%s] with value [%s] for AOT Repository processing.".formatted(entry.getKey(),
+ entry.getValue()));
+ }
+
+ properties.put(entry.getKey(), entry.getValue());
+ }
}
private Metamodel getMetamodel() {
@@ -141,6 +183,7 @@ public EntityManagerFactory getEntityManagerFactory() {
* A {@link Dialect} to satisfy the bootstrap requirements of {@link JdbcSettings#DIALECT} during the AOT Phase. Printed
* to log files (info level) when the {@link org.hibernate.engine.jdbc.env.spi.JdbcEnvironment} is created.
*/
+ @NullUnmarked
@SuppressWarnings("deprecation")
static class SpringDataJpaAotDialect extends Dialect {
@@ -164,6 +207,12 @@ public SequenceSupport getSequenceSupport() {
return ANSISequenceSupport.INSTANCE;
}
+ @Override
+ public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
+ // javadoc implies null would trigger default which is not the case
+ return new StandardSqlAstTranslatorFactory();
+ }
+
@Override
@SuppressWarnings("deprecation")
public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) {
@@ -175,4 +224,14 @@ public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalT
}
+ static class NoOpConnectionProvider extends UserSuppliedConnectionProviderImpl {
+
+ static final NoOpConnectionProvider INSTANCE = new NoOpConnectionProvider();
+
+ @Override
+ public String toString() {
+ return "NoOpConnectionProvider";
+ }
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java
index 3f0d2de2f7..545bb2f0d7 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java
@@ -169,6 +169,7 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class> returnedT
return source;
}
+ @SuppressWarnings("NullAway")
protected long getCount(Query query) {
List> totals = query.getResultList();
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java
index d90ecbc1e1..d9d4b9715a 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java
@@ -645,7 +645,7 @@ public CodeBlock build() {
TypeName typeToRead = isProjecting ? methodReturn.getActualTypeName() : TypeName.get(context.getDomainType());
builder.add("\n");
- if (modifying.isPresent() && !aotQuery.isDerived()) {
+ if (modifying.isPresent() && aotQuery !=null && !aotQuery.isDerived()) {
if (modifying.getBoolean("flushAutomatically")) {
builder.addStatement("this.$L.flush()", context.fieldNameOf(EntityManager.class));
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
index bd55bf3ec0..9090b07bc0 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
@@ -35,7 +35,10 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.hibernate.cfg.MappingSettings;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,6 +57,8 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.dao.DataAccessException;
@@ -419,6 +424,7 @@ protected void configureTypeContribution(Class> type, AotContext aotContext) {
return new JpaRepositoryContributor(repositoryContext, emf);
}
+ JpaProperties properties = new JpaProperties(environment);
ObjectProvider managedTypesProvider = beanFactory
.getBeanProvider(PersistenceManagedTypes.class);
PersistenceManagedTypes managedTypes = managedTypesProvider.getIfUnique();
@@ -426,7 +432,8 @@ protected void configureTypeContribution(Class> type, AotContext aotContext) {
if (managedTypes != null) {
log.debug("Using PersistenceManagedTypes for AOT repository generation");
- return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(managedTypes));
+ return contribute(repositoryContext,
+ AotEntityManagerFactoryCreator.from(managedTypes, properties.getJpaProperties()));
}
ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class);
@@ -435,11 +442,13 @@ protected void configureTypeContribution(Class> type, AotContext aotContext) {
if (unitInfo != null) {
log.debug("Using PersistenceUnitInfo for AOT repository generation");
- return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(unitInfo));
+ return contribute(repositoryContext,
+ AotEntityManagerFactoryCreator.from(unitInfo, properties.getJpaProperties()));
}
log.debug("Using scanned types for AOT repository generation");
- return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(repositoryContext));
+ return contribute(repositoryContext,
+ AotEntityManagerFactoryCreator.from(repositoryContext, properties.getJpaProperties()));
}
private JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext,
@@ -449,4 +458,59 @@ private JpaRepositoryContributor contribute(AotRepositoryContext repositoryConte
}
+ static class JpaProperties {
+
+ private final Map jpaProperties;
+
+ public JpaProperties(Environment environment) {
+
+ this.jpaProperties = new LinkedHashMap<>();
+
+ String implicitStrategy = getFirstAvailable(environment, "spring.jpa.hibernate.naming.implicitStrategy",
+ "spring.jpa.hibernate.naming.implicit-strategy");
+ if (StringUtils.hasText(implicitStrategy)) {
+ jpaProperties.put(MappingSettings.IMPLICIT_NAMING_STRATEGY, implicitStrategy);
+ }
+
+ String physicalStrategy = getFirstAvailable(environment, "spring.jpa.hibernate.naming.physicalStrategy",
+ "spring.jpa.hibernate.naming.physical-strategy");
+ if (StringUtils.hasText(physicalStrategy)) {
+ jpaProperties.put(MappingSettings.PHYSICAL_NAMING_STRATEGY, physicalStrategy);
+ }
+
+ if (environment instanceof ConfigurableEnvironment ce) {
+
+ ce.getPropertySources().forEach(propertySource -> {
+
+ if (propertySource instanceof EnumerablePropertySource> eps) {
+
+ String prefix = "spring.jpa.properties.";
+ Map partialProperties = Stream.of(eps.getPropertyNames())
+ .filter(propertyName -> propertyName.startsWith(prefix))
+ .collect(Collectors.toMap(k -> k.substring(prefix.length()), propertySource::getProperty));
+
+ jpaProperties.putAll(partialProperties);
+ }
+ });
+ }
+ }
+
+ @Nullable
+ String getFirstAvailable(Environment environment, String... propertyNames) {
+
+ for (String propertyName : propertyNames) {
+ String value = environment.getProperty(propertyName);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public Map getJpaProperties() {
+ return jpaProperties;
+ }
+
+ }
+
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotMetamodelUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotMetamodelUnitTests.java
index 9cfa3fd1d2..e9f191f211 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotMetamodelUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotMetamodelUnitTests.java
@@ -15,14 +15,19 @@
*/
package org.springframework.data.jpa.repository.aot;
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
import org.junit.jupiter.api.Test;
+import org.springframework.data.jpa.repository.aot.AotMetamodel.NoOpConnectionProvider;
+import org.springframework.data.jpa.repository.aot.AotMetamodel.SpringDataJpaAotDialect;
/**
* Unit tests for {@link AotMetamodel}.
*
* @author Mark Paluch
+ * @author Christoph Strobl
*/
class AotMetamodelUnitTests {
@@ -31,4 +36,36 @@ void dialectSupportsSequences() {
assertThat(AotMetamodel.SpringDataJpaAotDialect.INSTANCE.getSequenceSupport().supportsSequences()).isTrue();
assertThat(AotMetamodel.SpringDataJpaAotDialect.INSTANCE.getSequenceSupport().supportsPooledSequences()).isTrue();
}
+
+ @Test // GH-4092
+ void intializesPropertiesWithDefaults() {
+
+ assertThat(AotMetamodel.initProperties(Map.of())) //
+ .containsEntry("hibernate.dialect", SpringDataJpaAotDialect.INSTANCE) //
+ .containsEntry("hibernate.boot.allow_jdbc_metadata_access", false) //
+ .containsEntry("hibernate.connection.provider_class", NoOpConnectionProvider.INSTANCE) //
+ .containsEntry("hibernate.jpa_callbacks.enabled", false) //
+ .containsEntry("hibernate.query.startup_check", false);
+ }
+
+ @Test // GH-4092
+ void allowsDialectOverridesViaProperties() {
+
+ assertThat(AotMetamodel.initProperties(Map.of("hibernate.dialect", "H2Dialect", "jpa.bla.bla", "42")))
+ .containsEntry("hibernate.dialect", "H2Dialect") //
+ .containsEntry("jpa.bla.bla", "42");
+ }
+
+ @Test // GH-4092
+ void preventsPropertyOverrides/* for cases we know cause trouble */() {
+
+ assertThat(AotMetamodel.initProperties(Map.of(//
+ "hibernate.boot.allow_jdbc_metadata_access", "true", //
+ "hibernate.connection.provider_class", "DatasourceConnectionProviderImpl", //
+ "hibernate.jpa_callbacks.enabled", "true", //
+ "hibernate.query.startup_check", "true"))).containsEntry("hibernate.boot.allow_jdbc_metadata_access", false) //
+ .containsEntry("hibernate.connection.provider_class", NoOpConnectionProvider.INSTANCE) //
+ .containsEntry("hibernate.jpa_callbacks.enabled", false) //
+ .containsEntry("hibernate.query.startup_check", false);
+ }
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java
index 6412c70f88..89c64c0f62 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java
@@ -23,7 +23,9 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.Map;
+import org.hibernate.cfg.MappingSettings;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -38,9 +40,11 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension.JpaProperties;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.instrument.classloading.ShadowingClassLoader;
+import org.springframework.mock.env.MockEnvironment;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
/**
@@ -149,6 +153,40 @@ void exposesJpaAotProcessor() {
.isEqualTo(JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor.class);
}
+ @Test // GH-4092
+ void collectsWellKnownPropertyNames() {
+
+ MockEnvironment environment = new MockEnvironment()
+ .withProperty("spring.jpa.hibernate.naming.implicit-strategy", "Implicit")
+ .withProperty("spring.jpa.hibernate.naming.physical-strategy", "Physical");
+
+ Map jpaProperties = new JpaProperties(environment).getJpaProperties();
+
+ assertThat(jpaProperties).containsEntry(MappingSettings.IMPLICIT_NAMING_STRATEGY, "Implicit");
+ assertThat(jpaProperties).containsEntry(MappingSettings.PHYSICAL_NAMING_STRATEGY, "Physical");
+
+ environment = new MockEnvironment().withProperty("spring.jpa.hibernate.naming.implicitStrategy", "Implicit")
+ .withProperty("spring.jpa.hibernate.naming.physicalStrategy", "Physical");
+
+ jpaProperties = new JpaProperties(environment).getJpaProperties();
+
+ assertThat(jpaProperties).containsEntry(MappingSettings.IMPLICIT_NAMING_STRATEGY, "Implicit");
+ assertThat(jpaProperties).containsEntry(MappingSettings.PHYSICAL_NAMING_STRATEGY, "Physical");
+ }
+
+ @Test // GH-4092
+ void collectsJpaPropertyNames() {
+
+ MockEnvironment environment = new MockEnvironment()
+ .withProperty("spring.jpa.properties." + MappingSettings.IMPLICIT_NAMING_STRATEGY, "Implicit")
+ .withProperty("spring.jpa.properties.foo", "bar");
+
+ Map jpaProperties = new JpaProperties(environment).getJpaProperties();
+
+ assertThat(jpaProperties).containsEntry(MappingSettings.IMPLICIT_NAMING_STRATEGY, "Implicit");
+ assertThat(jpaProperties).containsEntry("foo", "bar");
+ }
+
private void assertOnlyOnePersistenceAnnotationBeanPostProcessorRegistered(DefaultListableBeanFactory factory,
String expectedBeanName) {