Capture @EnableJpaRepositories configuration for AOT processing.

Closes #3838
This commit is contained in:
Mark Paluch
2025-05-15 12:04:06 +02:00
parent 88da07e555
commit 3f17014b9f
10 changed files with 191 additions and 47 deletions

View File

@@ -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<PersonRepository> 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;

View File

@@ -58,8 +58,16 @@ class AotMetamodel implements Metamodel {
private final Lazy<EntityManager> 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) {

View File

@@ -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<Class<QueryEnhancerSelector>> 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<Class<QueryEnhancerSelector>> getQueryEnhancerSelectorClass() {
return (Optional) context.getConfigurationSource().getAttribute("queryEnhancerSelector", Class.class)
.filter(it -> !it.equals(QueryEnhancerSelector.DefaultQueryEnhancerSelector.class));
}
@Override
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method) {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, getRepositoryInformation(), getProjectionFactory(),
persistenceProvider);
// meh!
QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR;
Optional<Class<QueryEnhancerSelector>> 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) {

View File

@@ -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<Character> 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> 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);

View File

@@ -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<PersistenceUnitManager> unitManagerProvider = beanFactory
.getBeanProvider(PersistenceUnitManager.class);
PersistenceUnitManager unitManager = unitManagerProvider.getIfAvailable();
Optional<String> 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<PersistenceManagedTypes> 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<PersistenceUnitInfo> infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class);
PersistenceUnitInfo unitInfo = infoProvider.getIfAvailable();
PersistenceUnitInfo unitInfo = infoProvider.getIfUnique();
if (unitInfo != null) {

View File

@@ -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

View File

@@ -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 'ö'");
}
}

View File

@@ -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<T> implements AotRepositoryContext {
private final StubRepositoryInformation repositoryInformation;
private final Class<T> repositoryInterface;
private final RepositoryConfigurationSource configurationSource;
public TestJpaAotRepositoryContext(Class<T> repositoryInterface, @Nullable RepositoryComposition composition) {
public TestJpaAotRepositoryContext(Class<T> 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<T> implements AotRepositoryContext {
return "JPA";
}
@Override
public RepositoryConfigurationSource getConfigurationSource() {
return configurationSource;
}
@Override
public Set<String> getBasePackages() {
return Set.of("org.springframework.data.dummy.repository.aot");

View File

@@ -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<String> getBasePackages() {
return Collections.singleton(this.getClass().getPackageName());

View File

@@ -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`