diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java index a8682d32c..1ec94e160 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java @@ -25,6 +25,7 @@ import java.util.Set; import org.hibernate.jpa.HibernatePersistenceProvider; import org.junit.platform.commons.annotation.Testable; +import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; @@ -37,16 +38,24 @@ import org.openjdk.jmh.annotations.Timeout; import org.openjdk.jmh.annotations.Warmup; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.benchmark.model.Person; import org.springframework.data.jpa.benchmark.model.Profile; import org.springframework.data.jpa.benchmark.repository.PersonRepository; import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; import org.springframework.data.jpa.repository.aot.TestJpaAotRepositoryContext; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.SampleConfig; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -74,7 +83,10 @@ public class AotRepositoryQueryMethodBenchmarks { public static Class aot; public static TestJpaAotRepositoryContext repositoryContext = new TestJpaAotRepositoryContext<>( - PersonRepository.class, null); + PersonRepository.class, null, + new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(SampleConfig.class), + EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), + Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); EntityManager entityManager; RepositoryComposition.RepositoryFragments fragments; 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 ee8ecd7f5..a19191eb1 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 @@ -58,8 +58,16 @@ class AotMetamodel implements Metamodel { private final Lazy entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager()); public AotMetamodel(AotRepositoryContext repositoryContext) { - this(repositoryContext.getResolvedTypes().stream().map(Class::getName) - .filter(name -> !name.startsWith("jakarta.persistence")).toList(), null); + this(repositoryContext.getResolvedTypes().stream().filter(AotMetamodel::isJakartaAnnotated).map(Class::getName) + .toList(), null); + } + + private static boolean isJakartaAnnotated(Class cls) { + + return cls.isAnnotationPresent(jakarta.persistence.Entity.class) + || cls.isAnnotationPresent(jakarta.persistence.Embeddable.class) + || cls.isAnnotationPresent(jakarta.persistence.MappedSuperclass.class) + || cls.isAnnotationPresent(jakarta.persistence.Converter.class); } public AotMetamodel(PersistenceManagedTypes managedTypes) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 41bf6ff6e..564b5a809 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -22,9 +22,11 @@ import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.reflect.Method; import java.util.Map; +import java.util.Optional; import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -70,6 +72,7 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final PersistenceProvider persistenceProvider; private final QueriesFactory queriesFactory; private final EntityGraphLookup entityGraphLookup; + private final AotRepositoryContext context; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { this(repositoryContext, new AotMetamodel(repositoryContext)); @@ -87,9 +90,10 @@ public class JpaRepositoryContributor extends RepositoryContributor { super(repositoryContext); + this.context = repositoryContext; this.metamodel = entityManagerFactory.getMetamodel(); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); - this.queriesFactory = new QueriesFactory(entityManagerFactory); + this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory); this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); } @@ -97,9 +101,11 @@ public class JpaRepositoryContributor extends RepositoryContributor { super(repositoryContext); + this.context = repositoryContext; this.metamodel = metamodel; this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()); - this.queriesFactory = new QueriesFactory(metamodel.getEntityManagerFactory(), metamodel); + this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), + metamodel.getEntityManagerFactory(), metamodel); this.entityGraphLookup = new EntityGraphLookup(metamodel.getEntityManagerFactory()); } @@ -111,25 +117,36 @@ public class JpaRepositoryContributor extends RepositoryContributor { @Override protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { - // TODO: BeanFactoryQueryRewriterProvider if there is a method using QueryRewriters. - constructorBuilder.addParameter("entityManager", EntityManager.class); constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class); - // TODO: Pick up the configured QueryEnhancerSelector + Optional> queryEnhancerSelector = getQueryEnhancerSelectorClass(); + constructorBuilder.customize(builder -> { - builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); + + if (queryEnhancerSelector.isPresent()) { + builder.addStatement("super(new T$(), context)", queryEnhancerSelector.get()); + } else { + builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); + } }); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Optional> getQueryEnhancerSelectorClass() { + return (Optional) context.getConfigurationSource().getAttribute("queryEnhancerSelector", Class.class) + .filter(it -> !it.equals(QueryEnhancerSelector.DefaultQueryEnhancerSelector.class)); + } + @Override protected @Nullable MethodContributor contributeQueryMethod(Method method) { JpaQueryMethod queryMethod = new JpaQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), persistenceProvider); - // meh! - QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR; + Optional> queryEnhancerSelectorClass = getQueryEnhancerSelectorClass(); + QueryEnhancerSelector selector = queryEnhancerSelectorClass.map(BeanUtils::instantiateClass) + .orElse(QueryEnhancerSelector.DEFAULT_SELECTOR); // no stored procedures for now. if (queryMethod.isProcedureQuery()) { @@ -183,7 +200,6 @@ public class JpaRepositoryContributor extends RepositoryContributor { TypeInformation returnType = getRepositoryInformation().getReturnType(method); boolean returnsCount = JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType.getType()); - boolean isVoid = ClassUtils.isVoidType(returnType.getType()); if (!returnsCount && !isVoid) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java index ee26bf0d0..299dc4741 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -24,6 +24,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -37,6 +38,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.query.*; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; @@ -53,14 +55,21 @@ class QueriesFactory { private final EntityManagerFactory entityManagerFactory; private final Metamodel metamodel; + private final EscapeCharacter escapeCharacter; + private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - public QueriesFactory(EntityManagerFactory entityManagerFactory) { - this(entityManagerFactory, entityManagerFactory.getMetamodel()); + public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory) { + this(configurationSource, entityManagerFactory, entityManagerFactory.getMetamodel()); } - public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metamodel) { + public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory, + Metamodel metamodel) { + this.metamodel = metamodel; this.entityManagerFactory = entityManagerFactory; + + Optional escapeCharacter = configurationSource.getAttribute("escapeCharacter", Character.class); + this.escapeCharacter = escapeCharacter.map(EscapeCharacter::of).orElse(EscapeCharacter.DEFAULT); } /** @@ -77,8 +86,7 @@ class QueriesFactory { QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, - queryMethod); + return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); } TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); @@ -201,9 +209,6 @@ class QueriesFactory { MergedAnnotation query, JpaQueryMethod queryMethod) { PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); - // TODO make configurable - JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { @@ -222,8 +227,7 @@ class QueriesFactory { private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, JpqlQueryTemplates templates) { - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, false, returnedType, metadataProvider, templates, metamodel); @@ -234,8 +238,7 @@ class QueriesFactory { private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, JpqlQueryTemplates templates) { - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel); 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 360b449be..95495accb 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 @@ -69,7 +69,6 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; -import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -339,38 +338,34 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi Environment environment = repositoryContext.getEnvironment(); - boolean enabled = Boolean.parseBoolean( - environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + boolean enabled = Boolean + .parseBoolean(environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); if (!enabled) { return null; } ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); - boolean useEntityManager = Boolean.parseBoolean( - environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); + boolean useEntityManager = Boolean + .parseBoolean(environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); if (useEntityManager) { - ObjectProvider unitManagerProvider = beanFactory - .getBeanProvider(PersistenceUnitManager.class); - PersistenceUnitManager unitManager = unitManagerProvider.getIfAvailable(); + Optional entityManagerFactoryRef = repositoryContext.getConfigurationSource() + .getAttribute("entityManagerFactoryRef"); - if (unitManager != null) { + log.debug( + "Using EntityManager '%s' for AOT repository generation".formatted(entityManagerFactoryRef.orElse(""))); - log.debug("Using PersistenceUnitManager for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, unitManager.obtainDefaultPersistenceUnitInfo()); - } - - log.debug("Using EntityManager for AOT repository generation"); - - EntityManagerFactory emf = beanFactory.getBean(EntityManagerFactory.class); + EntityManagerFactory emf = entityManagerFactoryRef + .map(it -> beanFactory.getBean(it, EntityManagerFactory.class)) + .orElseGet(() -> beanFactory.getBean(EntityManagerFactory.class)); return new JpaRepositoryContributor(repositoryContext, emf); } ObjectProvider managedTypesProvider = beanFactory .getBeanProvider(PersistenceManagedTypes.class); - PersistenceManagedTypes managedTypes = managedTypesProvider.getIfAvailable(); + PersistenceManagedTypes managedTypes = managedTypesProvider.getIfUnique(); if (managedTypes != null) { @@ -379,7 +374,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi } ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); - PersistenceUnitInfo unitInfo = infoProvider.getIfAvailable(); + PersistenceUnitInfo unitInfo = infoProvider.getIfUnique(); if (unitInfo != null) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java index b73f9cc0d..87243096d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java @@ -21,6 +21,8 @@ import jakarta.persistence.EntityManagerFactory; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import org.mockito.Mockito; + import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,11 +31,18 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.SampleConfig; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -56,8 +65,15 @@ class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { private final TestJpaAotRepositoryContext repositoryContext; public AotFragmentTestConfigurationSupport(Class repositoryInterface) { + this(repositoryInterface, SampleConfig.class); + } + + public AotFragmentTestConfigurationSupport(Class repositoryInterface, Class configClass) { this.repositoryInterface = repositoryInterface; - this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null); + this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null, + new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass), + EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), + Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java new file mode 100644 index 000000000..97334b00c --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * https://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.data.jpa.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.UrlResource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * Integration tests for the {@link UserRepository} AOT fragment. + * + * @author Mark Paluch + */ +class JpaRepositoryContributorConfigurationTests { + + @Configuration + static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + public JpaRepositoryContributorConfiguration() { + super(UserRepository.class, MyConfiguration.class); + } + + @EnableJpaRepositories(escapeCharacter = 'ö', /* avoid creating repository instances */ includeFilters = { + @ComponentScan.Filter(value = EnableJpaRepositories.class) }) + static class MyConfiguration { + + } + } + + @Test // GH-3838 + void shouldConsiderConfiguration() throws IOException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(JpaRepositoryContributorConfiguration.class); + context.refreshForAotProcessing(new RuntimeHints()); + + String location = UserRepository.class.getPackageName().replace('.', '/') + "/" + + UserRepository.class.getSimpleName() + ".json"; + UrlResource resource = new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location)); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'streamByLastnameLike')].query").isArray().first().isObject() + .containsEntry("query", + "SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname LIKE :lastname ESCAPE 'ö'"); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index 6fc63defa..b46528c4a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -32,6 +32,7 @@ import org.springframework.core.test.tools.ClassFile; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.lang.Nullable; @@ -45,9 +46,12 @@ public class TestJpaAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Class repositoryInterface; + private final RepositoryConfigurationSource configurationSource; - public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition, + RepositoryConfigurationSource configurationSource) { this.repositoryInterface = repositoryInterface; + this.configurationSource = configurationSource; this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } @@ -85,6 +89,11 @@ public class TestJpaAotRepositoryContext implements AotRepositoryContext { return "JPA"; } + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return configurationSource; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java index d75531c50..82c06f968 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.config; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -46,6 +47,7 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.AotRepositoryInformation; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.javapoet.ClassName; @@ -155,6 +157,11 @@ class JpaRepositoryRegistrationAotProcessorUnitTests { return "JPA"; } + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return mock(RepositoryConfigurationSource.class); + } + @Override public Set getBasePackages() { return Collections.singleton(this.getClass().getPackageName()); diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index 031a75f52..e8d2d4a1a 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -53,6 +53,12 @@ For instance, profiles that have been enabled at build-time are automatically en Also, the Spring Data module implementing a repository is fixed. Changing the implementation requires AOT re-processing. +NOTE: AOT processing avoids database access. +Therefore, it initializes an in-memory Hibernate instance for metadata collection. +Types for the Hibernate configuration are determined by our AOT metadata collector. +We prefer using a `PersistentEntityTypes` bean if available and fall back to `PersistenceUnitInfo` or our own discovered types. +If our type scanning is not sufficient for your arrangement, you can enable direct `EntityManagerFactory` usage by configuring the `spring.aot.jpa.repositories.use-entitymanager=true` property. + === Eligible Methods AOT repositories filter methods that are eligible for AOT processing. @@ -69,7 +75,6 @@ These are typically all query methods that are not backed by an xref:repositorie * Value Expressions (Those require a bit of reflective information. Mind that using Value Expressions requires expression parsing and contextual information to evaluate the expression) - **Limitations** * Requires Hibernate for AOT processing. @@ -79,8 +84,7 @@ Mind that using Value Expressions requires expression parsing and contextual inf **Excluded methods** -* `CrudRepository` and other base interface methods -* Querydsl and Query by Example methods +* `CrudRepository`, Querydsl, Query by Example, and other base interface methods as their implementation is provided by the base class respective fragments * Methods whose implementation would be overly complex ** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) ** Stored procedure query methods annotated with `@Procedure`