Skip to content

Commit 8ec2b60

Browse files
committed
Add support for projections.
See #3830
1 parent 1b242dd commit 8ec2b60

File tree

11 files changed

+1120
-638
lines changed

11 files changed

+1120
-638
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ abstract class AotQuery {
4040
*/
4141
public abstract boolean isNative();
4242

43+
/**
44+
* @return the list of parameter bindings.
45+
*/
4346
public List<ParameterBinding> getParameterBindings() {
4447
return parameterBindings;
4548
}

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

+53
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,24 @@
1515
*/
1616
package org.springframework.data.jpa.repository.aot;
1717

18+
import jakarta.persistence.Tuple;
19+
1820
import java.lang.reflect.Method;
21+
import java.util.Collection;
22+
import java.util.stream.Stream;
1923

2024
import org.jspecify.annotations.Nullable;
2125

26+
import org.springframework.core.CollectionFactory;
27+
import org.springframework.data.domain.Slice;
2228
import org.springframework.data.domain.Sort;
2329
import org.springframework.data.expression.ValueEvaluationContextProvider;
2430
import org.springframework.data.expression.ValueExpression;
2531
import org.springframework.data.jpa.repository.query.DeclaredQuery;
2632
import org.springframework.data.jpa.repository.query.JpaParameters;
2733
import org.springframework.data.jpa.repository.query.QueryEnhancer;
2834
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
35+
import org.springframework.data.jpa.util.TupleBackedMap;
2936
import org.springframework.data.projection.ProjectionFactory;
3037
import org.springframework.data.repository.core.RepositoryMetadata;
3138
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
@@ -104,6 +111,52 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class<?> returnedT
104111
return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies()));
105112
}
106113

114+
protected <T> @Nullable T convertOne(@Nullable Object result, boolean nativeQuery, Class<T> projection) {
115+
116+
if (result == null) {
117+
return null;
118+
}
119+
120+
if (projection.isInstance(result)) {
121+
return projection.cast(result);
122+
}
123+
124+
return projectionFactory.createProjection(projection,
125+
result instanceof Tuple t ? new TupleBackedMap(nativeQuery ? TupleBackedMap.underscoreAware(t) : t) : result);
126+
}
127+
128+
protected @Nullable Object convertMany(@Nullable Object result, boolean nativeQuery, Class<?> projection) {
129+
130+
if (result == null) {
131+
return null;
132+
}
133+
134+
if (projection.isInstance(result)) {
135+
return result;
136+
}
137+
138+
if (result instanceof Stream<?> stream) {
139+
return stream.map(it -> convertOne(it, nativeQuery, projection));
140+
}
141+
142+
if (result instanceof Slice<?> slice) {
143+
return slice.map(it -> convertOne(it, nativeQuery, projection));
144+
}
145+
146+
if (result instanceof Collection<?> collection) {
147+
148+
Collection<@Nullable Object> target = CollectionFactory.createCollection(collection.getClass(),
149+
collection.size());
150+
for (Object o : collection) {
151+
target.add(convertOne(o, nativeQuery, projection));
152+
}
153+
154+
return target;
155+
}
156+
157+
throw new UnsupportedOperationException("Cannot create projection for %s".formatted(result));
158+
}
159+
107160
private record DefaultQueryRewriteInformation(Sort sort,
108161
ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation {
109162

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.aot;
17+
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.EntityManagerFactory;
20+
21+
import java.util.Arrays;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.LinkedHashSet;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Set;
28+
29+
import org.jspecify.annotations.Nullable;
30+
31+
import org.springframework.core.annotation.AnnotatedElementUtils;
32+
import org.springframework.core.annotation.MergedAnnotation;
33+
import org.springframework.data.jpa.repository.EntityGraph;
34+
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
35+
import org.springframework.data.repository.core.RepositoryInformation;
36+
import org.springframework.data.repository.query.ReturnedType;
37+
import org.springframework.util.StringUtils;
38+
39+
/**
40+
* Factory for {@link AotEntityGraph}.
41+
*
42+
* @author Mark Paluch
43+
* @since 4.0
44+
*/
45+
class EntityGraphLookup {
46+
47+
private final EntityManagerFactory entityManagerFactory;
48+
49+
public EntityGraphLookup(EntityManagerFactory entityManagerFactory) {
50+
this.entityManagerFactory = entityManagerFactory;
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
public @Nullable AotEntityGraph findEntityGraph(MergedAnnotation<EntityGraph> entityGraph,
55+
RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) {
56+
57+
if (!entityGraph.isPresent()) {
58+
return null;
59+
}
60+
61+
EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class);
62+
String[] attributePaths = entityGraph.getStringArray("attributePaths");
63+
Collection<String> entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod);
64+
List<Class<?>> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(),
65+
returnedType.getTypeToRead());
66+
67+
for (Class<?> candidate : candidates) {
68+
69+
Map<String, jakarta.persistence.EntityGraph<?>> namedEntityGraphs = entityManagerFactory
70+
.getNamedEntityGraphs(Class.class.cast(candidate));
71+
72+
if (namedEntityGraphs.isEmpty()) {
73+
continue;
74+
}
75+
76+
for (String entityGraphName : entityGraphNames) {
77+
if (namedEntityGraphs.containsKey(entityGraphName)) {
78+
return new AotEntityGraph(entityGraphName, type, Collections.emptyList());
79+
}
80+
}
81+
}
82+
83+
if (attributePaths.length > 0) {
84+
return new AotEntityGraph(null, type, Arrays.asList(attributePaths));
85+
}
86+
87+
return null;
88+
}
89+
90+
private Set<String> getEntityGraphNames(MergedAnnotation<EntityGraph> entityGraph, RepositoryInformation information,
91+
JpaQueryMethod queryMethod) {
92+
93+
Set<String> entityGraphNames = new LinkedHashSet<>();
94+
String value = entityGraph.getString("value");
95+
96+
if (StringUtils.hasText(value)) {
97+
entityGraphNames.add(value);
98+
}
99+
entityGraphNames.add(queryMethod.getNamedQueryName());
100+
entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod));
101+
return entityGraphNames;
102+
}
103+
104+
private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) {
105+
106+
Class<?> domainType = information.getDomainType();
107+
Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class);
108+
String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name()
109+
: domainType.getSimpleName();
110+
111+
return entityName + "." + queryMethod.getName();
112+
}
113+
114+
}

0 commit comments

Comments
 (0)