Skip to content

Commit eb12668

Browse files
committed
Add support for JSON repository metadata.
See #3830
1 parent be4d852 commit eb12668

File tree

11 files changed

+410
-86
lines changed

11 files changed

+410
-86
lines changed

spring-data-jpa/pom.xml

+20-5
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,31 @@
8888
<optional>true</optional>
8989
</dependency>
9090

91-
<dependency>
92-
<groupId>org.junit.platform</groupId>
93-
<artifactId>junit-platform-launcher</artifactId>
94-
<scope>test</scope>
95-
</dependency>
91+
<dependency>
92+
<groupId>org.junit.platform</groupId>
93+
<artifactId>junit-platform-launcher</artifactId>
94+
<scope>test</scope>
95+
</dependency>
96+
9697
<dependency>
9798
<groupId>org.springframework</groupId>
9899
<artifactId>spring-core-test</artifactId>
99100
<scope>test</scope>
100101
</dependency>
102+
103+
<dependency>
104+
<groupId>net.javacrumbs.json-unit</groupId>
105+
<artifactId>json-unit-assertj</artifactId>
106+
<version>4.1.0</version>
107+
<scope>test</scope>
108+
</dependency>
109+
110+
<dependency>
111+
<groupId>com.fasterxml.jackson.core</groupId>
112+
<artifactId>jackson-databind</artifactId>
113+
<scope>test</scope>
114+
</dependency>
115+
101116
<dependency>
102117
<groupId>org.hsqldb</groupId>
103118
<artifactId>hsqldb</artifactId>

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java

+51
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
package org.springframework.data.jpa.repository.aot;
1717

1818

19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
1921
import java.util.function.Function;
2022

2123
import org.jspecify.annotations.Nullable;
2224

2325
import org.springframework.data.jpa.repository.query.DeclaredQuery;
2426
import org.springframework.data.jpa.repository.query.QueryEnhancer;
2527
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
28+
import org.springframework.data.repository.aot.generate.QueryMetadata;
2629
import org.springframework.util.StringUtils;
2730

2831
/**
@@ -68,4 +71,52 @@ public boolean isNative() {
6871
return result().isNative();
6972
}
7073

74+
public QueryMetadata toMetadata(boolean paging) {
75+
return new AotQueryMetadata(paging);
76+
}
77+
78+
/**
79+
* String and Named Query-based {@link QueryMetadata}.
80+
*/
81+
private class AotQueryMetadata implements QueryMetadata {
82+
83+
private final boolean paging;
84+
85+
AotQueryMetadata(boolean paging) {
86+
this.paging = paging;
87+
}
88+
89+
@Override
90+
public Map<String, Object> serialize() {
91+
92+
Map<String, Object> serialized = new LinkedHashMap<>();
93+
94+
if (result() instanceof NamedAotQuery nq) {
95+
96+
serialized.put("name", nq.getName());
97+
serialized.put("query", nq.getQueryString());
98+
}
99+
100+
if (result() instanceof StringAotQuery sq) {
101+
serialized.put("query", sq.getQueryString());
102+
}
103+
104+
if (paging) {
105+
106+
if (count() instanceof NamedAotQuery nq) {
107+
108+
serialized.put("count-name", nq.getName());
109+
serialized.put("count-query", nq.getQueryString());
110+
}
111+
112+
if (count() instanceof StringAotQuery sq) {
113+
serialized.put("count-query", sq.getQueryString());
114+
}
115+
}
116+
117+
return serialized;
118+
}
119+
120+
}
121+
71122
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java

+58-6
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
import jakarta.persistence.EntityManagerFactory;
2020

2121
import java.lang.reflect.Method;
22+
import java.util.Map;
2223

2324
import org.jspecify.annotations.Nullable;
2425

26+
import org.springframework.core.annotation.AnnotatedElementUtils;
2527
import org.springframework.core.annotation.MergedAnnotation;
28+
import org.springframework.core.annotation.MergedAnnotations;
2629
import org.springframework.data.jpa.provider.PersistenceProvider;
2730
import org.springframework.data.jpa.repository.EntityGraph;
2831
import org.springframework.data.jpa.repository.Modifying;
@@ -31,10 +34,12 @@
3134
import org.springframework.data.jpa.repository.QueryHints;
3235
import org.springframework.data.jpa.repository.query.JpaParameters;
3336
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
37+
import org.springframework.data.jpa.repository.query.Procedure;
3438
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
3539
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
3640
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata;
3741
import org.springframework.data.repository.aot.generate.MethodContributor;
42+
import org.springframework.data.repository.aot.generate.QueryMetadata;
3843
import org.springframework.data.repository.aot.generate.RepositoryContributor;
3944
import org.springframework.data.repository.config.AotRepositoryContext;
4045
import org.springframework.data.repository.core.RepositoryInformation;
@@ -46,6 +51,7 @@
4651
import org.springframework.javapoet.TypeName;
4752
import org.springframework.javapoet.TypeSpec;
4853
import org.springframework.util.ClassUtils;
54+
import org.springframework.util.StringUtils;
4955

5056
/**
5157
* JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager}
@@ -113,20 +119,50 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB
113119

114120
// no stored procedures for now.
115121
if (queryMethod.isProcedureQuery()) {
122+
123+
Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class);
124+
125+
MethodContributor.QueryMethodMetadataContributorBuilder<JpaQueryMethod> builder = MethodContributor
126+
.forQueryMethod(queryMethod);
127+
128+
129+
if (procedure != null) {
130+
131+
if (StringUtils.hasText(procedure.name())) {
132+
return builder.metadataOnly(new NamedStoredProcedureMetadata(procedure.name()));
133+
}
134+
135+
if (StringUtils.hasText(procedure.procedureName())) {
136+
return builder.metadataOnly(new StoredProcedureMetadata(procedure.procedureName()));
137+
}
138+
139+
if (StringUtils.hasText(procedure.value())) {
140+
return builder.metadataOnly(new StoredProcedureMetadata(procedure.value()));
141+
}
142+
}
143+
144+
// TODO: Better fallback.
116145
return null;
117146
}
118147

119148
ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType();
120149
JpaParameters parameters = queryMethod.getParameters();
121150

151+
MergedAnnotation<Query> query = MergedAnnotations.from(method).get(Query.class);
152+
153+
AotQueries aotQueries = queriesFactory.createQueries(repositoryInformation, query, selector, queryMethod,
154+
returnedType);
155+
122156
// no KeysetScrolling for now.
123157
if (parameters.hasScrollPositionParameter()) {
124-
return null;
158+
return MethodContributor.forQueryMethod(queryMethod)
159+
.metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
125160
}
126161

127162
// no dynamic projections.
128163
if (parameters.hasDynamicProjection()) {
129-
return null;
164+
return MethodContributor.forQueryMethod(queryMethod)
165+
.metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
130166
}
131167

132168
if (queryMethod.isModifyingQuery()) {
@@ -138,23 +174,23 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB
138174
boolean isVoid = ClassUtils.isVoidType(returnType.getType());
139175

140176
if (!returnsCount && !isVoid) {
141-
return null;
177+
return MethodContributor.forQueryMethod(queryMethod)
178+
.metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery()));
142179
}
143180
}
144181

145-
return MethodContributor.forQueryMethod(queryMethod).contribute(context -> {
182+
return MethodContributor.forQueryMethod(queryMethod).withMetadata(aotQueries.toMetadata(queryMethod.isPageQuery()))
183+
.contribute(context -> {
146184

147185
CodeBlock.Builder body = CodeBlock.builder();
148186

149-
MergedAnnotation<Query> query = context.getAnnotation(Query.class);
150187
MergedAnnotation<NativeQuery> nativeQuery = context.getAnnotation(NativeQuery.class);
151188
MergedAnnotation<QueryHints> queryHints = context.getAnnotation(QueryHints.class);
152189
MergedAnnotation<EntityGraph> entityGraph = context.getAnnotation(EntityGraph.class);
153190
MergedAnnotation<Modifying> modifying = context.getAnnotation(Modifying.class);
154191

155192
body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
156193

157-
AotQueries aotQueries = queriesFactory.createQueries(context, query, selector, queryMethod, returnedType);
158194
AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation,
159195
returnedType, queryMethod);
160196

@@ -170,4 +206,20 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB
170206
});
171207
}
172208

209+
record StoredProcedureMetadata(String procedure) implements QueryMetadata {
210+
211+
@Override
212+
public Map<String, Object> serialize() {
213+
return Map.of("procedure", procedure());
214+
}
215+
}
216+
217+
record NamedStoredProcedureMetadata(String procedureName) implements QueryMetadata {
218+
219+
@Override
220+
public Map<String, Object> serialize() {
221+
return Map.of("procedure-name", procedureName());
222+
}
223+
}
224+
173225
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030
class NamedAotQuery extends AotQuery {
3131

3232
private final String name;
33-
private final DeclaredQuery queryString;
33+
private final DeclaredQuery query;
3434

3535
private NamedAotQuery(String name, DeclaredQuery queryString, List<ParameterBinding> parameterBindings) {
3636
super(parameterBindings);
3737
this.name = name;
38-
this.queryString = queryString;
38+
this.query = queryString;
3939
}
4040

4141
/**
@@ -51,13 +51,17 @@ public String getName() {
5151
return name;
5252
}
5353

54-
public DeclaredQuery getQueryString() {
55-
return queryString;
54+
public DeclaredQuery getQuery() {
55+
return query;
56+
}
57+
58+
public String getQueryString() {
59+
return getQuery().getQueryString();
5660
}
5761

5862
@Override
5963
public boolean isNative() {
60-
return queryString.isNative();
64+
return query.isNative();
6165
}
6266

6367
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metam
7373
* @param returnedType
7474
* @return
7575
*/
76-
public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedAnnotation<Query> query,
76+
public AotQueries createQueries(RepositoryInformation repositoryInformation, MergedAnnotation<Query> query,
7777
QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) {
7878

7979
if (query.isPresent() && StringUtils.hasText(query.getString("value"))) {
80-
return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query,
80+
return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query,
8181
queryMethod);
8282
}
8383

@@ -86,7 +86,7 @@ public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedA
8686
return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod);
8787
}
8888

89-
return buildPartTreeQuery(returnedType, context, query, queryMethod);
89+
return buildPartTreeQuery(returnedType, repositoryInformation, query, queryMethod);
9090
}
9191

9292
private AotQueries buildStringQuery(Class<?> domainType, ReturnedType returnedType, QueryEnhancerSelector selector,
@@ -159,7 +159,7 @@ private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelec
159159

160160
String countProjection = query.isPresent() ? query.getString("countProjection") : null;
161161
return AotQueries.from(aotQuery, it -> {
162-
return StringAotQuery.of(aotQuery.getQueryString()).getQuery();
162+
return StringAotQuery.of(aotQuery.getQuery()).getQuery();
163163
}, countProjection, selector);
164164
}
165165

@@ -197,10 +197,10 @@ private NamedAotQuery buildNamedAotQuery(TypedQueryReference<?> namedQuery, JpaQ
197197
return null;
198198
}
199199

200-
private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context,
200+
private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInformation repositoryInformation,
201201
MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
202202

203-
PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType());
203+
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
204204
// TODO make configurable
205205
JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
206206

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.data.repository.core.support.QueryCreationListener;
4848
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
4949
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
50+
import org.springframework.data.repository.core.support.RepositoryFragment;
5051
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
5152
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
5253
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -298,7 +299,8 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata
298299
getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata);
299300
invokeAwareMethods(querydslJpaPredicateExecutor);
300301

301-
return RepositoryFragments.just(querydslJpaPredicateExecutor);
302+
return RepositoryFragments
303+
.of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor));
302304
}
303305

304306
return RepositoryFragments.empty();

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020

2121
import java.util.function.Function;
2222

23+
import org.jspecify.annotations.Nullable;
24+
2325
import org.springframework.beans.BeanUtils;
2426
import org.springframework.beans.factory.BeanFactory;
2527
import org.springframework.beans.factory.ObjectProvider;
26-
27-
import org.jspecify.annotations.Nullable;
2828
import org.springframework.beans.factory.annotation.Autowired;
2929
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
3030
import org.springframework.data.jpa.repository.query.EscapeCharacter;
@@ -54,7 +54,7 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
5454

5555
private @Nullable BeanFactory beanFactory;
5656
private @Nullable EntityManager entityManager;
57-
private EntityPathResolver entityPathResolver;
57+
private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE;
5858
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
5959
private @Nullable JpaQueryMethodFactory queryMethodFactory;
6060
private @Nullable Function<BeanFactory, QueryEnhancerSelector> queryEnhancerSelectorSource;

0 commit comments

Comments
 (0)