diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 12a089e3e..1cc667406 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -88,16 +88,31 @@
true
-
- org.junit.platform
- junit-platform-launcher
- test
-
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
+
org.springframework
spring-core-test
test
+
+
+ net.javacrumbs.json-unit
+ json-unit-assertj
+ 4.1.0
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ test
+
+
org.hsqldb
hsqldb
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java
index 0b900c72a..51d639ea7 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java
@@ -16,6 +16,8 @@
package org.springframework.data.jpa.repository.aot;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
@@ -23,6 +25,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.QueryEnhancer;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
+import org.springframework.data.repository.aot.generate.QueryMetadata;
import org.springframework.util.StringUtils;
/**
@@ -68,4 +71,52 @@ record AotQueries(AotQuery result, AotQuery count) {
return result().isNative();
}
+ public QueryMetadata toMetadata(boolean paging) {
+ return new AotQueryMetadata(paging);
+ }
+
+ /**
+ * String and Named Query-based {@link QueryMetadata}.
+ */
+ private class AotQueryMetadata implements QueryMetadata {
+
+ private final boolean paging;
+
+ AotQueryMetadata(boolean paging) {
+ this.paging = paging;
+ }
+
+ @Override
+ public Map serialize() {
+
+ Map serialized = new LinkedHashMap<>();
+
+ if (result() instanceof NamedAotQuery nq) {
+
+ serialized.put("name", nq.getName());
+ serialized.put("query", nq.getQueryString());
+ }
+
+ if (result() instanceof StringAotQuery sq) {
+ serialized.put("query", sq.getQueryString());
+ }
+
+ if (paging) {
+
+ if (count() instanceof NamedAotQuery nq) {
+
+ serialized.put("count-name", nq.getName());
+ serialized.put("count-query", nq.getQueryString());
+ }
+
+ if (count() instanceof StringAotQuery sq) {
+ serialized.put("count-query", sq.getQueryString());
+ }
+ }
+
+ return serialized;
+ }
+
+ }
+
}
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 b5ab48031..785c9133c 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
@@ -19,10 +19,13 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.lang.reflect.Method;
+import java.util.Map;
import org.jspecify.annotations.Nullable;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Modifying;
@@ -31,10 +34,12 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
+import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata;
import org.springframework.data.repository.aot.generate.MethodContributor;
+import org.springframework.data.repository.aot.generate.QueryMetadata;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
@@ -46,6 +51,7 @@ import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
/**
* JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager}
@@ -113,20 +119,50 @@ public class JpaRepositoryContributor extends RepositoryContributor {
// no stored procedures for now.
if (queryMethod.isProcedureQuery()) {
+
+ Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class);
+
+ MethodContributor.QueryMethodMetadataContributorBuilder builder = MethodContributor
+ .forQueryMethod(queryMethod);
+
+
+ if (procedure != null) {
+
+ if (StringUtils.hasText(procedure.name())) {
+ return builder.metadataOnly(new NamedStoredProcedureMetadata(procedure.name()));
+ }
+
+ if (StringUtils.hasText(procedure.procedureName())) {
+ return builder.metadataOnly(new StoredProcedureMetadata(procedure.procedureName()));
+ }
+
+ if (StringUtils.hasText(procedure.value())) {
+ return builder.metadataOnly(new StoredProcedureMetadata(procedure.value()));
+ }
+ }
+
+ // TODO: Better fallback.
return null;
}
ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType();
JpaParameters parameters = queryMethod.getParameters();
+ MergedAnnotation query = MergedAnnotations.from(method).get(Query.class);
+
+ AotQueries aotQueries = queriesFactory.createQueries(repositoryInformation, query, selector, queryMethod,
+ returnedType);
+
// no KeysetScrolling for now.
if (parameters.hasScrollPositionParameter()) {
- return null;
+ return MethodContributor.forQueryMethod(queryMethod)
+ .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
}
// no dynamic projections.
if (parameters.hasDynamicProjection()) {
- return null;
+ return MethodContributor.forQueryMethod(queryMethod)
+ .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
}
if (queryMethod.isModifyingQuery()) {
@@ -138,15 +174,16 @@ public class JpaRepositoryContributor extends RepositoryContributor {
boolean isVoid = ClassUtils.isVoidType(returnType.getType());
if (!returnsCount && !isVoid) {
- return null;
+ return MethodContributor.forQueryMethod(queryMethod)
+ .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
}
}
- return MethodContributor.forQueryMethod(queryMethod).contribute(context -> {
+ return MethodContributor.forQueryMethod(queryMethod).withMetadata(aotQueries.toMetadata(queryMethod.isPageQuery()))
+ .contribute(context -> {
CodeBlock.Builder body = CodeBlock.builder();
- MergedAnnotation query = context.getAnnotation(Query.class);
MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class);
MergedAnnotation queryHints = context.getAnnotation(QueryHints.class);
MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class);
@@ -154,7 +191,6 @@ public class JpaRepositoryContributor extends RepositoryContributor {
body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
- AotQueries aotQueries = queriesFactory.createQueries(context, query, selector, queryMethod, returnedType);
AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation,
returnedType, queryMethod);
@@ -170,4 +206,20 @@ public class JpaRepositoryContributor extends RepositoryContributor {
});
}
+ record StoredProcedureMetadata(String procedure) implements QueryMetadata {
+
+ @Override
+ public Map serialize() {
+ return Map.of("procedure", procedure());
+ }
+ }
+
+ record NamedStoredProcedureMetadata(String procedureName) implements QueryMetadata {
+
+ @Override
+ public Map serialize() {
+ return Map.of("procedure-name", procedureName());
+ }
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java
index 3f7b9293b..e3813ce13 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java
@@ -30,12 +30,12 @@ import org.springframework.data.jpa.repository.query.PreprocessedQuery;
class NamedAotQuery extends AotQuery {
private final String name;
- private final DeclaredQuery queryString;
+ private final DeclaredQuery query;
private NamedAotQuery(String name, DeclaredQuery queryString, List parameterBindings) {
super(parameterBindings);
this.name = name;
- this.queryString = queryString;
+ this.query = queryString;
}
/**
@@ -51,13 +51,17 @@ class NamedAotQuery extends AotQuery {
return name;
}
- public DeclaredQuery getQueryString() {
- return queryString;
+ public DeclaredQuery getQuery() {
+ return query;
+ }
+
+ public String getQueryString() {
+ return getQuery().getQueryString();
}
@Override
public boolean isNative() {
- return queryString.isNative();
+ return query.isNative();
}
}
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 f31d437fc..05c49f114 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
@@ -73,11 +73,11 @@ class QueriesFactory {
* @param returnedType
* @return
*/
- public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedAnnotation query,
+ public AotQueries createQueries(RepositoryInformation repositoryInformation, MergedAnnotation query,
QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) {
if (query.isPresent() && StringUtils.hasText(query.getString("value"))) {
- return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query,
+ return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query,
queryMethod);
}
@@ -86,7 +86,7 @@ class QueriesFactory {
return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod);
}
- return buildPartTreeQuery(returnedType, context, query, queryMethod);
+ return buildPartTreeQuery(returnedType, repositoryInformation, query, queryMethod);
}
private AotQueries buildStringQuery(Class> domainType, ReturnedType returnedType, QueryEnhancerSelector selector,
@@ -159,7 +159,7 @@ class QueriesFactory {
String countProjection = query.isPresent() ? query.getString("countProjection") : null;
return AotQueries.from(aotQuery, it -> {
- return StringAotQuery.of(aotQuery.getQueryString()).getQuery();
+ return StringAotQuery.of(aotQuery.getQuery()).getQuery();
}, countProjection, selector);
}
@@ -197,10 +197,10 @@ class QueriesFactory {
return null;
}
- private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context,
+ private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInformation repositoryInformation,
MergedAnnotation query, JpaQueryMethod queryMethod) {
- PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType());
+ PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
// TODO make configurable
JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
index 2e24577f8..91314ed11 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
@@ -47,6 +47,7 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -298,7 +299,8 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata);
invokeAwareMethods(querydslJpaPredicateExecutor);
- return RepositoryFragments.just(querydslJpaPredicateExecutor);
+ return RepositoryFragments
+ .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor));
}
return RepositoryFragments.empty();
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
index 8e8200a37..a9d8622a4 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
@@ -20,11 +20,11 @@ import jakarta.persistence.PersistenceContext;
import java.util.function.Function;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
-
-import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
@@ -54,7 +54,7 @@ public class JpaRepositoryFactoryBean, S, ID>
private @Nullable BeanFactory beanFactory;
private @Nullable EntityManager entityManager;
- private EntityPathResolver entityPathResolver;
+ private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
private @Nullable JpaQueryMethodFactory queryMethodFactory;
private @Nullable Function queryEnhancerSelectorSource;
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java
index 9cbd86109..566a08cb7 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java
@@ -103,21 +103,21 @@ class JpaRepositoryContributorIntegrationTests {
em.clear();
}
- @Test
+ @Test // GH-3830
void testDerivedFinderWithoutArguments() {
List users = fragment.findUserNoArgumentsBy();
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
}
- @Test
+ @Test // GH-3830
void testFindDerivedQuerySingleEntity() {
User user = fragment.findOneByEmailAddress("luke@jedi.org");
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
- @Test
+ @Test // GH-3830
void testFindDerivedFinderOptionalEntity() {
Optional user = fragment.findOptionalOneByEmailAddress("yoda@jedi.org");
@@ -125,21 +125,21 @@ class JpaRepositoryContributorIntegrationTests {
.hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda"));
}
- @Test
+ @Test // GH-3830
void testDerivedCount() {
Long value = fragment.countUsersByLastname("Skywalker");
assertThat(value).isEqualTo(2L);
}
- @Test
+ @Test // GH-3830
void testDerivedExists() {
Boolean exists = fragment.existsUserByLastname("Skywalker");
assertThat(exists).isTrue();
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningList() {
List users = fragment.findByLastnameStartingWith("S");
@@ -147,7 +147,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "han@smuggler.net");
}
- @Test
+ @Test // GH-3830
void shouldReturnStream() {
Stream users = fragment.streamByLastnameLike("S%");
@@ -155,14 +155,14 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "han@smuggler.net");
}
- @Test
+ @Test // GH-3830
void testLimitedDerivedFinder() {
List users = fragment.findTop2ByLastnameStartingWith("S");
assertThat(users).hasSize(2);
}
- @Test
+ @Test // GH-3830
void testSortedDerivedFinder() {
List users = fragment.findByLastnameStartingWithOrderByEmailAddress("S");
@@ -170,14 +170,14 @@ class JpaRepositoryContributorIntegrationTests {
"luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderWithLimitArgument() {
List users = fragment.findByLastnameStartingWith("S", Limit.of(2));
assertThat(users).hasSize(2);
}
- @Test
+ @Test // GH-3830
void testDerivedFinderWithSort() {
List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"));
@@ -185,21 +185,21 @@ class JpaRepositoryContributorIntegrationTests {
"luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderWithSortAndLimit() {
List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"), Limit.of(2));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningListWithPageable() {
List users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningPage() {
Page page = fragment.findPageOfUsersByLastnameStartingWith("S",
@@ -211,7 +211,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningSlice() {
Slice slice = fragment.findSliceOfUserByLastnameStartingWith("S",
@@ -223,14 +223,14 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderReturningSingleValueWithQuery() {
User user = fragment.findAnnotatedQueryByEmailAddress("yoda@jedi.org");
assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderReturningListWithQuery() {
List users = fragment.findAnnotatedQueryByLastname("S");
@@ -238,7 +238,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() {
List users = fragment.findAnnotatedQueryByLastnameParameter("S");
@@ -246,7 +246,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void shouldApplyAnnotatedLikeStartsEnds() {
// start with case
@@ -260,7 +260,7 @@ class JpaRepositoryContributorIntegrationTests {
"chewie@smuggler.net", "yoda@jedi.org");
}
- @Test
+ @Test // GH-3830
void testAnnotatedMultilineFinderWithQuery() {
List users = fragment.findAnnotatedMultilineQueryByLastname("S");
@@ -268,14 +268,14 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderWithQueryAndLimit() {
List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2));
assertThat(users).hasSize(2);
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderWithQueryAndSort() {
List users = fragment.findAnnotatedQueryByLastname("S", Sort.by("emailAddress"));
@@ -283,21 +283,21 @@ class JpaRepositoryContributorIntegrationTests {
"luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderWithQueryLimitAndSort() {
List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("emailAddress"));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderReturningListWithPageable() {
List users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderReturningPage() {
Page page = fragment.findAnnotatedQueryPageOfUsersByLastname("S",
@@ -309,7 +309,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void testPagingAnnotatedQueryWithSort() {
Page page = fragment.findAnnotatedQueryPageWithStaticSort("S", PageRequest.of(0, 2, Sort.unsorted()));
@@ -320,7 +320,7 @@ class JpaRepositoryContributorIntegrationTests {
"vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testAnnotatedFinderReturningSlice() {
Slice slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S",
@@ -331,7 +331,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com");
}
- @Test
+ @Test // GH-3830
void shouldResolveTemplatedQuery() {
User user = fragment.findTemplatedByEmailAddress("han@smuggler.net");
@@ -340,7 +340,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getFirstname()).isEqualTo("Han");
}
- @Test
+ @Test // GH-3830
void shouldEvaluateExpressionByName() {
User user = fragment.findValueExpressionNamedByEmailAddress("han@smuggler.net");
@@ -349,7 +349,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getFirstname()).isEqualTo("Han");
}
- @Test
+ @Test // GH-3830
void shouldEvaluateExpressionByPosition() {
User user = fragment.findValueExpressionPositionalByEmailAddress("han@smuggler.net");
@@ -358,7 +358,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getFirstname()).isEqualTo("Han");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningListOfProjections() {
List users = fragment.findUserProjectionByLastnameStartingWith("S");
@@ -366,7 +366,7 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
- @Test
+ @Test // GH-3830
void testDerivedFinderReturningPageOfProjections() {
Page page = fragment.findUserProjectionByLastnameStartingWith("S",
@@ -383,7 +383,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(noResults).isEmpty();
}
- @Test
+ @Test // GH-3830
void shouldApplySqlResultSetMapping() {
User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId());
@@ -391,7 +391,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyNamedDto() {
// named queries cannot be rewritten
@@ -399,7 +399,7 @@ class JpaRepositoryContributorIntegrationTests {
.isThrownBy(() -> fragment.findNamedDtoEmailAddress(kylo.getEmailAddress()));
}
- @Test
+ @Test // GH-3830
void shouldApplyDerivedDto() {
UserRepository.Names names = fragment.findDtoByEmailAddress(kylo.getEmailAddress());
@@ -408,7 +408,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(names.firstname()).isEqualTo(kylo.getFirstname());
}
- @Test
+ @Test // GH-3830
void shouldApplyDerivedDtoPage() {
Page names = fragment.findDtoPageByEmailAddress(kylo.getEmailAddress(), PageRequest.of(0, 1));
@@ -417,7 +417,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname());
}
- @Test
+ @Test // GH-3830
void shouldApplyAnnotatedDto() {
UserRepository.Names names = fragment.findAnnotatedDtoEmailAddress(kylo.getEmailAddress());
@@ -426,7 +426,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(names.firstname()).isEqualTo(kylo.getFirstname());
}
- @Test
+ @Test // GH-3830
void shouldApplyAnnotatedDtoPage() {
Page names = fragment.findAnnotatedDtoPageByEmailAddress(kylo.getEmailAddress(),
@@ -436,7 +436,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname());
}
- @Test
+ @Test // GH-3830
void shouldApplyDerivedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findEmailProjectionById(kylo.getId());
@@ -444,7 +444,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyInterfaceProjectionPage() {
Page result = fragment.findProjectedPageByEmailAddress(kylo.getEmailAddress(),
@@ -454,7 +454,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyInterfaceProjectionSlice() {
Slice result = fragment.findProjectedSliceByEmailAddress(kylo.getEmailAddress(),
@@ -464,7 +464,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyInterfaceProjectionToDerivedQueryStream() {
Stream result = fragment.streamProjectedByEmailAddress(kylo.getEmailAddress());
@@ -472,7 +472,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result).hasSize(1).map(UserRepository.EmailOnly::getEmailAddress).contains(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyAnnotatedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findAnnotatedEmailProjectionByEmailAddress(kylo.getEmailAddress());
@@ -480,7 +480,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyAnnotatedInterfaceProjectionQueryPage() {
Page result = fragment.findAnnotatedProjectedPageByEmailAddress(kylo.getEmailAddress(),
@@ -490,7 +490,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyNativeInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findEmailProjectionByNativeQuery(kylo.getId());
@@ -498,7 +498,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void shouldApplyNamedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findNamedProjectionEmailAddress(kylo.getEmailAddress());
@@ -506,7 +506,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
- @Test
+ @Test // GH-3830
void testDerivedDeleteSingle() {
User result = fragment.deleteByEmailAddress("yoda@jedi.org");
@@ -519,14 +519,14 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(yodaShouldBeGone).isNull();
}
- @Test
+ @Test // GH-3830
void shouldOmitAnnotatedDeleteReturningDomainType() {
assertThatException().isThrownBy(() -> fragment.deleteAnnotatedQueryByEmailAddress("foo"))
.withRootCauseInstanceOf(NoSuchMethodException.class);
}
- @Test
+ @Test // GH-3830
void shouldApplyModifying() {
int affected = fragment.renameAllUsersTo("Jones");
@@ -539,7 +539,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(yodaShouldBeGone).isNull();
}
- @Test
+ @Test // GH-3830
void nativeQuery() {
Page page = fragment.findByNativeQueryWithPageable(PageRequest.of(0, 2));
@@ -549,14 +549,14 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(page.getContent()).containsExactly("Anakin", "Ben");
}
- @Test
+ @Test // GH-3830
void shouldUseNamedQuery() {
User user = fragment.findByEmailAddress("luke@jedi.org");
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
- @Test
+ @Test // GH-3830
void shouldUseNamedQueryAndDeriveCountQuery() {
Page user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
@@ -565,7 +565,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getTotalElements()).isEqualTo(1);
}
- @Test
+ @Test // GH-3830
void shouldUseNamedQueryAndProvidedCountQuery() {
Page user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
@@ -574,7 +574,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getTotalElements()).isEqualTo(1);
}
- @Test
+ @Test // GH-3830
void shouldUseNamedQueryAndNamedCountQuery() {
Page user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
@@ -583,13 +583,13 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getTotalElements()).isEqualTo(1);
}
- @Test
+ @Test // GH-3830
void shouldApplyQueryHints() {
assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker"))
.withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo");
}
- @Test
+ @Test // GH-3830
void shouldApplyNamedEntityGraph() {
User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca");
@@ -598,7 +598,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class);
}
- @Test
+ @Test // GH-3830
void shouldApplyDeclaredEntityGraph() {
User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca");
@@ -610,7 +610,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(han.getManager()).isInstanceOf(HibernateProxy.class);
}
- @Test
+ @Test // GH-3830
void shouldQuerySubtype() {
SpecialUser snoopy = new SpecialUser();
@@ -625,7 +625,7 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(result).isInstanceOf(SpecialUser.class);
}
- @Test
+ @Test // GH-3830
void shouldApplyQueryRewriter() {
User result = fragment.findAndApplyQueryRewriter(kylo.getEmailAddress());
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java
new file mode 100644
index 000000000..0cdde1ef0
--- /dev/null
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java
@@ -0,0 +1,178 @@
+/*
+ * 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.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Integration tests for the {@link UserRepository} JSON metadata.
+ *
+ * @author Mark Paluch
+ */
+@SpringJUnitConfig(classes = JpaRepositoryMetadataIntegrationTests.JpaRepositoryContributorConfiguration.class)
+@Transactional
+class JpaRepositoryMetadataIntegrationTests {
+
+ @Autowired AbstractApplicationContext context;
+
+ @Configuration
+ static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+ public JpaRepositoryContributorConfiguration() {
+ super(UserRepository.class);
+ }
+ }
+
+ @Test // GH-3830
+ void shouldDocumentBase() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).isObject() //
+ .containsEntry("name", UserRepository.class.getName()) //
+ .containsEntry("module", "") // TODO: JPA should be here
+ .containsEntry("type", "IMPERATIVE");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentDerivedQuery() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[0]").isObject().containsEntry("name", "countUsersByLastname");
+ assertThatJson(json).inPath("$.methods[0].query").isObject().containsEntry("query",
+ "SELECT COUNT(u) FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname = ?1");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentPagedQuery() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'findAndApplyQueryRewriter')].query").isArray().element(1)
+ .isObject().containsEntry("query", "select u from OTHER u where u.emailAddress = ?1")
+ .containsEntry("count-query", "select count(u) from OTHER u where u.emailAddress = ?1");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentQueryWithExpression() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'findValueExpressionNamedByEmailAddress')].query").isArray()
+ .first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentNamedQuery() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'findPagedWithNamedCountByEmailAddress')].query").isArray()
+ .first().isObject().containsEntry("name", "User.findByEmailAddress")
+ .containsEntry("query", "SELECT u FROM User u WHERE u.emailAddress = ?1")
+ .containsEntry("count-name", "User.findByEmailAddress.count-provided")
+ .containsEntry("count-query", "SELECT count(u) FROM User u WHERE u.emailAddress = ?1");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentNamedProcedure() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'namedProcedure')].query").isArray().first().isObject()
+ .containsEntry("procedure-name", "User.plus1IO");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentProvidedProcedure() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'providedProcedure')].query").isArray().first().isObject()
+ .containsEntry("procedure", "sp_add");
+ }
+
+ @Test // GH-3830
+ void shouldDocumentBaseFragment() throws IOException {
+
+ Resource resource = getResource();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.exists()).isTrue();
+
+ String json = resource.getContentAsString(StandardCharsets.UTF_8);
+
+ assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject()
+ .containsEntry("fragment", "org.springframework.data.jpa.repository.support.SimpleJpaRepository");
+ }
+
+ private Resource getResource() {
+
+ String location = UserRepository.class.getPackageName().replace('.', '/') + "/"
+ + UserRepository.class.getSimpleName() + ".json";
+ return new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location));
+ }
+
+}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java
index f9c4042d3..589b95a5f 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java
@@ -107,7 +107,12 @@ class StubRepositoryInformation implements RepositoryInformation {
@Override
public boolean isQueryMethod(Method method) {
- return false;
+
+ if (isBaseClassMethod(method)) {
+ return false;
+ }
+
+ return true;
}
@Override
@@ -124,4 +129,10 @@ class StubRepositoryInformation implements RepositoryInformation {
public Method getTargetClassMethod(Method method) {
return null;
}
+
+ @Override
+ public RepositoryComposition getRepositoryComposition() {
+ return baseComposition;
+ }
+
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java
index 3d5eeb930..b95cd8837 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java
@@ -33,7 +33,9 @@ import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.QueryRewriter;
+import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
/**
* @author Christoph Strobl
@@ -236,6 +238,15 @@ interface UserRepository extends CrudRepository {
@Query(value = "select u from OTHER u where u.emailAddress = ?1", queryRewriter = MyQueryRewriter.class)
Page findAndApplyQueryRewriter(String emailAddress, Pageable pageable);
+ // -------------------------------------------------------------------------
+ // Unsupported: Procedures
+ // -------------------------------------------------------------------------
+ @Procedure(name = "User.plus1IO") // Named
+ Integer namedProcedure(@Param("arg") Integer arg);
+
+ @Procedure(value = "sp_add") // Stored procedure
+ Integer providedProcedure(@Param("arg") Integer arg);
+
interface EmailOnly {
String getEmailAddress();
}