Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 83ba970

Browse files
schaudergregturn
authored andcommittedNov 4, 2021
Translate projected properties of the fluent query API into a fetchgraph.
When a property path based projection is specified we still return the root entity. But we do provide a fetchgraph. The JPA implementation will (should) load only the specified attributes eagerly. It most likely will also load all other attributes from all selected tables. Once we have infrastructure in place for for multilevel projections the same approach can and should be used for those. Currently this is not the case. Closes #2329 Original pull request: #2345.
1 parent 43305f6 commit 83ba970

File tree

10 files changed

+513
-86
lines changed

10 files changed

+513
-86
lines changed
 

‎src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
20+
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.function.Function;
2223
import java.util.stream.Stream;
@@ -36,7 +37,6 @@
3637
import org.springframework.data.mapping.context.MappingContext;
3738
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
3839
import org.springframework.data.support.PageableExecutionUtils;
39-
import org.springframework.lang.Nullable;
4040
import org.springframework.util.Assert;
4141

4242
/**
@@ -47,38 +47,42 @@
4747
* @param <R> Result type
4848
* @author Greg Turnquist
4949
* @author Mark Paluch
50+
* @author Jens Schauder
5051
* @since 2.6
5152
*/
52-
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
53+
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
5354

5455
private final Example<S> example;
5556
private final Function<Sort, TypedQuery<S>> finder;
5657
private final Function<Example<S>, Long> countOperation;
5758
private final Function<Example<S>, Boolean> existsOperation;
5859
private final EntityManager entityManager;
5960
private final EscapeCharacter escapeCharacter;
61+
private final Projector<TypedQuery<?>> projector;
6062

6163
public FetchableFluentQueryByExample(Example<S> example, Function<Sort, TypedQuery<S>> finder,
6264
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
6365
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
6466
EntityManager entityManager, EscapeCharacter escapeCharacter) {
65-
this(example, (Class<R>) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation,
66-
context, entityManager, escapeCharacter);
67+
this(example, example.getProbeType(), (Class<R>) example.getProbeType(), Sort.unsorted(), Collections.emptySet(),
68+
finder, countOperation, existsOperation, context, entityManager, escapeCharacter,
69+
new TypedQueryProjector(entityManager));
6770
}
6871

69-
private FetchableFluentQueryByExample(Example<S> example, Class<R> returnType, Sort sort,
70-
@Nullable Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
71-
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
72+
private FetchableFluentQueryByExample(Example<S> example, Class<S> entityType, Class<R> returnType, Sort sort,
73+
Collection<String> properties, Function<Sort, TypedQuery<S>> finder, Function<Example<S>, Long> countOperation,
74+
Function<Example<S>, Boolean> existsOperation,
7275
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
73-
EntityManager entityManager, EscapeCharacter escapeCharacter) {
76+
EntityManager entityManager, EscapeCharacter escapeCharacter, Projector<TypedQuery<?>> projector) {
7477

75-
super(returnType, sort, properties, context);
78+
super(returnType, sort, properties, context, entityType);
7679
this.example = example;
7780
this.finder = finder;
7881
this.countOperation = countOperation;
7982
this.existsOperation = existsOperation;
8083
this.entityManager = entityManager;
8184
this.escapeCharacter = escapeCharacter;
85+
this.projector = projector;
8286
}
8387

8488
/*
@@ -90,8 +94,9 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
9094

9195
Assert.notNull(sort, "Sort must not be null!");
9296

93-
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort.and(sort), this.properties,
94-
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
97+
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort.and(sort), properties, finder,
98+
countOperation, existsOperation, context, entityManager, escapeCharacter,
99+
new TypedQueryProjector(entityManager));
95100
}
96101

97102
/*
@@ -106,8 +111,9 @@ public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
106111
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
107112
}
108113

109-
return new FetchableFluentQueryByExample<>(this.example, resultType, this.sort, this.properties, this.finder,
110-
this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
114+
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder,
115+
countOperation, existsOperation, context, entityManager, escapeCharacter,
116+
new TypedQueryProjector(entityManager));
111117
}
112118

113119
/*
@@ -117,8 +123,9 @@ public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
117123
@Override
118124
public FetchableFluentQuery<R> project(Collection<String> properties) {
119125

120-
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort, mergeProperties(properties),
121-
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
126+
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties),
127+
finder, countOperation, existsOperation, context, entityManager, escapeCharacter,
128+
new TypedQueryProjector(entityManager));
122129
}
123130

124131
/*
@@ -128,7 +135,7 @@ public FetchableFluentQuery<R> project(Collection<String> properties) {
128135
@Override
129136
public R oneValue() {
130137

131-
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
138+
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery();
132139
limitedQuery.setMaxResults(2); // Never need more than 2 values
133140

134141
List<S> results = limitedQuery.getResultList();
@@ -147,7 +154,7 @@ public R oneValue() {
147154
@Override
148155
public R firstValue() {
149156

150-
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
157+
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery();
151158
limitedQuery.setMaxResults(1); // Never need more than 1 value
152159

153160
List<S> results = limitedQuery.getResultList();
@@ -162,7 +169,7 @@ public R firstValue() {
162169
@Override
163170
public List<R> all() {
164171

165-
List<S> resultList = this.finder.apply(this.sort).getResultList();
172+
List<S> resultList = createSortedAndProjectedQuery().getResultList();
166173

167174
return convert(resultList);
168175
}
@@ -183,7 +190,7 @@ public Page<R> page(Pageable pageable) {
183190
@Override
184191
public Stream<R> stream() {
185192

186-
return this.finder.apply(this.sort) //
193+
return createSortedAndProjectedQuery() //
187194
.getResultStream() //
188195
.map(getConversionFunction());
189196
}
@@ -194,7 +201,7 @@ public Stream<R> stream() {
194201
*/
195202
@Override
196203
public long count() {
197-
return this.countOperation.apply(example);
204+
return countOperation.apply(example);
198205
}
199206

200207
/*
@@ -203,12 +210,12 @@ public long count() {
203210
*/
204211
@Override
205212
public boolean exists() {
206-
return this.existsOperation.apply(example);
213+
return existsOperation.apply(example);
207214
}
208215

209216
private Page<R> readPage(Pageable pageable) {
210217

211-
TypedQuery<S> pagedQuery = this.finder.apply(this.sort);
218+
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();
212219

213220
if (pageable.isPaged()) {
214221
pagedQuery.setFirstResult((int) pageable.getOffset());
@@ -217,7 +224,15 @@ private Page<R> readPage(Pageable pageable) {
217224

218225
List<R> paginatedResults = convert(pagedQuery.getResultList());
219226

220-
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example));
227+
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example));
228+
}
229+
230+
private TypedQuery<S> createSortedAndProjectedQuery() {
231+
232+
TypedQuery<S> query = finder.apply(sort);
233+
projector.apply(entityType, query, properties);
234+
235+
return query;
221236
}
222237

223238
private List<R> convert(List<S> resultList) {
@@ -232,7 +247,7 @@ private List<R> convert(List<S> resultList) {
232247
}
233248

234249
private Function<Object, R> getConversionFunction() {
235-
return getConversionFunction(this.example.getProbeType(), this.resultType);
250+
return getConversionFunction(example.getProbeType(), resultType);
236251
}
237252

238253
}

‎src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
20+
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.function.BiFunction;
2223
import java.util.function.Function;
@@ -32,11 +33,10 @@
3233
import org.springframework.data.mapping.context.MappingContext;
3334
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
3435
import org.springframework.data.support.PageableExecutionUtils;
35-
import org.springframework.lang.Nullable;
3636
import org.springframework.util.Assert;
3737

3838
import com.querydsl.core.types.Predicate;
39-
import com.querydsl.jpa.JPQLQuery;
39+
import com.querydsl.jpa.impl.AbstractJPAQuery;
4040

4141
/**
4242
* Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that
@@ -46,38 +46,41 @@
4646
* @param <R> Result type
4747
* @author Greg Turnquist
4848
* @author Mark Paluch
49+
* @author Jens Schauder
4950
* @since 2.6
5051
*/
51-
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
52+
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
5253

5354
private final Predicate predicate;
54-
private final Function<Sort, JPQLQuery<S>> finder;
55-
private final BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder;
55+
private final Function<Sort, AbstractJPAQuery<?, ?>> finder;
56+
private final BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder;
5657
private final Function<Predicate, Long> countOperation;
5758
private final Function<Predicate, Boolean> existsOperation;
58-
private final Class<S> entityType;
59-
60-
public FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Function<Sort, JPQLQuery<S>> finder,
61-
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
62-
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
63-
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
64-
this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType,
65-
context);
59+
private final Projector<AbstractJPAQuery<?, ?>> projector;
60+
61+
public FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType,
62+
Function<Sort, AbstractJPAQuery<?, ?>> finder, BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
63+
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
64+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
65+
Projector<AbstractJPAQuery<?, ?>> projector) {
66+
this(predicate, entityType, (Class<R>) entityType, Sort.unsorted(), Collections.emptySet(), finder, pagedFinder,
67+
countOperation, existsOperation, context, projector);
6668
}
6769

68-
private FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Sort sort,
69-
@Nullable Collection<String> properties, Function<Sort, JPQLQuery<S>> finder,
70-
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
71-
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
72-
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
70+
private FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType, Class<R> resultType, Sort sort,
71+
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
72+
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
73+
Function<Predicate, Boolean> existsOperation,
74+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
75+
Projector<AbstractJPAQuery<?, ?>> projector) {
7376

74-
super(resultType, sort, properties, context);
77+
super(resultType, sort, properties, context, entityType);
7578
this.predicate = predicate;
7679
this.finder = finder;
7780
this.pagedFinder = pagedFinder;
7881
this.countOperation = countOperation;
7982
this.existsOperation = existsOperation;
80-
this.entityType = entityType;
83+
this.projector = projector;
8184
}
8285

8386
/*
@@ -89,8 +92,8 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
8992

9093
Assert.notNull(sort, "Sort must not be null!");
9194

92-
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort.and(sort), this.properties,
93-
this.finder, this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
95+
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort.and(sort), properties, finder,
96+
pagedFinder, countOperation, existsOperation, context, projector);
9497
}
9598

9699
/*
@@ -101,12 +104,13 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
101104
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
102105

103106
Assert.notNull(resultType, "Projection target type must not be null!");
107+
104108
if (!resultType.isInterface()) {
105109
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
106110
}
107111

108-
return new FetchableFluentQueryByPredicate<>(this.predicate, resultType, this.sort, this.properties, this.finder,
109-
this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
112+
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, properties, finder,
113+
pagedFinder, countOperation, existsOperation, context, projector);
110114
}
111115

112116
/*
@@ -116,9 +120,8 @@ public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
116120
@Override
117121
public FetchableFluentQuery<R> project(Collection<String> properties) {
118122

119-
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort,
120-
mergeProperties(properties), this.finder, this.pagedFinder, this.countOperation, this.existsOperation,
121-
this.entityType, this.context);
123+
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, mergeProperties(properties),
124+
finder, pagedFinder, countOperation, existsOperation, context, projector);
122125
}
123126

124127
/*
@@ -128,7 +131,7 @@ public FetchableFluentQuery<R> project(Collection<String> properties) {
128131
@Override
129132
public R oneValue() {
130133

131-
List<S> results = this.finder.apply(this.sort) //
134+
List<?> results = createSortedAndProjectedQuery() //
132135
.limit(2) // Never need more than 2 values
133136
.fetch();
134137

@@ -146,7 +149,7 @@ public R oneValue() {
146149
@Override
147150
public R firstValue() {
148151

149-
List<S> results = this.finder.apply(this.sort) //
152+
List<?> results = createSortedAndProjectedQuery() //
150153
.limit(1) // Never need more than 1 value
151154
.fetch();
152155

@@ -159,9 +162,7 @@ public R firstValue() {
159162
*/
160163
@Override
161164
public List<R> all() {
162-
163-
JPQLQuery<S> query = this.finder.apply(this.sort);
164-
return convert(query.fetch());
165+
return convert(createSortedAndProjectedQuery().fetch());
165166
}
166167

167168
/*
@@ -180,7 +181,7 @@ public Page<R> page(Pageable pageable) {
180181
@Override
181182
public Stream<R> stream() {
182183

183-
return this.finder.apply(this.sort) //
184+
return createSortedAndProjectedQuery() //
184185
.stream() //
185186
.map(getConversionFunction());
186187
}
@@ -191,7 +192,7 @@ public Stream<R> stream() {
191192
*/
192193
@Override
193194
public long count() {
194-
return this.countOperation.apply(this.predicate);
195+
return countOperation.apply(predicate);
195196
}
196197

197198
/*
@@ -200,31 +201,38 @@ public long count() {
200201
*/
201202
@Override
202203
public boolean exists() {
203-
return this.existsOperation.apply(this.predicate);
204+
return existsOperation.apply(predicate);
205+
}
206+
207+
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {
208+
209+
final AbstractJPAQuery<?, ?> query = finder.apply(sort);
210+
projector.apply(entityType, query, properties);
211+
return query;
204212
}
205213

206214
private Page<R> readPage(Pageable pageable) {
207215

208-
JPQLQuery<S> pagedQuery = this.pagedFinder.apply(this.sort, pageable);
216+
AbstractJPAQuery<?, ?> pagedQuery = pagedFinder.apply(sort, pageable);
209217
List<R> paginatedResults = convert(pagedQuery.fetch());
210218

211-
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate));
219+
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(predicate));
212220
}
213221

214-
private List<R> convert(List<S> resultList) {
222+
private List<R> convert(List<?> resultList) {
215223

216224
Function<Object, R> conversionFunction = getConversionFunction();
217225
List<R> mapped = new ArrayList<>(resultList.size());
218226

219-
for (S s : resultList) {
220-
mapped.add(conversionFunction.apply(s));
227+
for (Object o : resultList) {
228+
mapped.add(conversionFunction.apply(o));
221229
}
222230

223231
return mapped;
224232
}
225233

226234
private Function<Object, R> getConversionFunction() {
227-
return getConversionFunction(this.entityType, this.resultType);
235+
return getConversionFunction(entityType, resultType);
228236
}
229237

230238
}

‎src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,45 +34,46 @@
3434
*
3535
* @param <R> The resulting type of the query.
3636
* @author Greg Turnquist
37+
* @author Jens Schauder
3738
* @since 2.6
3839
*/
39-
abstract class FluentQuerySupport<R> {
40+
abstract class FluentQuerySupport<S, R> {
4041

4142
protected final Class<R> resultType;
4243
protected final Sort sort;
4344
/** Properties on which the query projects. {@literal null} stands for no special projection. */
44-
protected final @Nullable Set<String> properties;
45+
protected final Set<String> properties;
4546
protected final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
47+
protected final Class<S> entityType;
4648

4749
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
4850

4951
FluentQuerySupport(Class<R> resultType, Sort sort, @Nullable Collection<String> properties,
50-
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
52+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context, Class<S> entityType) {
5153

5254
this.resultType = resultType;
5355
this.sort = sort;
5456

5557
if (properties != null) {
5658
this.properties = new HashSet<>(properties);
5759
} else {
58-
this.properties = null;
60+
this.properties = new HashSet<>();
5961
}
6062

6163
this.context = context;
64+
this.entityType = entityType;
6265
}
6366

6467
final Collection<String> mergeProperties(Collection<String> additionalProperties) {
6568

6669
Set<String> newProperties = new HashSet<>();
67-
if (this.properties != null) {
68-
newProperties.addAll(this.properties);
69-
}
70+
newProperties.addAll(properties);
7071
newProperties.addAll(additionalProperties);
7172
return Collections.unmodifiableCollection(newProperties);
7273
}
7374

7475
@SuppressWarnings("unchecked")
75-
final <S> Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> targetType) {
76+
final Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> targetType) {
7677

7778
if (targetType.isAssignableFrom(inputType)) {
7879
return (Function<Object, R>) Function.identity();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2021 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.support;
17+
18+
import java.util.Set;
19+
20+
import javax.persistence.EntityGraph;
21+
import javax.persistence.EntityManager;
22+
import javax.persistence.Subgraph;
23+
24+
import org.springframework.data.mapping.PropertyPath;
25+
26+
/**
27+
* Turns a collection of property paths to an {@link EntityGraph} and applies it to a query abstraction
28+
*
29+
* @param <Q> the type of the query abstraction.
30+
* @author Jens Schauder
31+
* @since 2.6
32+
*/
33+
abstract class Projector<Q> {
34+
35+
private final EntityManager entityManager;
36+
37+
protected Projector(EntityManager entityManager) {
38+
this.entityManager = entityManager;
39+
}
40+
41+
public void apply(Class<?> domainType, Q query, Set<String> properties) {
42+
43+
if (!properties.isEmpty()) {
44+
45+
final javax.persistence.EntityGraph<?> entityGraph = entityManager.createEntityGraph(domainType);
46+
47+
for (String property : properties) {
48+
49+
Subgraph<Object> subgraph = null;
50+
51+
for (PropertyPath path : PropertyPath.from(property, domainType)) {
52+
53+
if (path.hasNext()) {
54+
subgraph = subgraph == null ? entityGraph.addSubgraph(path.getSegment())
55+
: subgraph.addSubgraph(path.getSegment());
56+
} else {
57+
58+
if (subgraph == null) {
59+
entityGraph.addAttributeNodes(path.getSegment());
60+
} else {
61+
subgraph.addAttributeNodes(path.getSegment());
62+
}
63+
}
64+
}
65+
}
66+
67+
applyEntityGraph(query, entityGraph);
68+
}
69+
}
70+
71+
abstract void applyEntityGraph(Q query, EntityGraph<?> entityGraph);
72+
}

‎src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
6969
/**
7070
* Creates a new {@link QuerydslJpaPredicateExecutor} from the given domain class and {@link EntityManager} and uses
7171
* the given {@link EntityPathResolver} to translate the domain class into an {@link EntityPath}.
72-
*
72+
*
7373
* @param entityInformation must not be {@literal null}.
7474
* @param entityManager must not be {@literal null}.
7575
* @param resolver must not be {@literal null}.
@@ -178,36 +178,36 @@ public <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQu
178178
Assert.notNull(predicate, "Predicate must not be null!");
179179
Assert.notNull(queryFunction, "Query function must not be null!");
180180

181-
Function<Sort, JPQLQuery<T>> finder = sort -> {
182-
JPQLQuery<T> select = createQuery(predicate).select(path);
181+
Function<Sort, AbstractJPAQuery<?, ?>> finder = sort -> {
182+
AbstractJPAQuery<?, ?> select = (AbstractJPAQuery<?, ?>) createQuery(predicate).select(path);
183183

184184
if (sort != null) {
185-
select = querydsl.applySorting(sort, select);
185+
select = (AbstractJPAQuery<?, ?>) querydsl.applySorting(sort, select);
186186
}
187187

188188
return select;
189189
};
190190

191-
BiFunction<Sort, Pageable, JPQLQuery<T>> pagedFinder = (sort, pageable) -> {
191+
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder = (sort, pageable) -> {
192192

193-
JPQLQuery<T> select = finder.apply(sort);
193+
AbstractJPAQuery<?, ?> select = finder.apply(sort);
194194

195195
if (pageable.isPaged()) {
196-
select = querydsl.applyPagination(pageable, select);
196+
select = (AbstractJPAQuery<?, ?>) querydsl.applyPagination(pageable, select);
197197
}
198198

199199
return select;
200200
};
201201

202202
FetchableFluentQueryByPredicate<T, T> fluentQuery = new FetchableFluentQueryByPredicate<>( //
203203
predicate, //
204-
entityInformation.getJavaType(), //
204+
this.entityInformation.getJavaType(), //
205205
finder, //
206206
pagedFinder, //
207207
this::count, //
208208
this::exists, //
209-
this.entityInformation.getJavaType(), //
210-
new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel())) //
209+
new JpaMetamodelMappingContext(Collections.singleton(this.entityManager.getMetamodel())), //
210+
new QuerydslProjector(entityManager) //
211211
);
212212

213213
return queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
@@ -237,7 +237,7 @@ public boolean exists(Predicate predicate) {
237237
* @param predicate
238238
* @return the Querydsl {@link JPQLQuery}.
239239
*/
240-
protected JPQLQuery<?> createQuery(Predicate... predicate) {
240+
protected AbstractJPAQuery<?, ?> createQuery(Predicate... predicate) {
241241

242242
Assert.notNull(predicate, "Predicate must not be null!");
243243

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021 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.support;
17+
18+
import javax.persistence.EntityGraph;
19+
import javax.persistence.EntityManager;
20+
21+
import com.querydsl.jpa.impl.AbstractJPAQuery;
22+
23+
/**
24+
* Applies fetchgraph hints to {@code AbstractJPAQuery}.
25+
*
26+
* @author Jens Schauder
27+
* @since 2.6
28+
*/
29+
class QuerydslProjector extends Projector<AbstractJPAQuery<?, ?>> {
30+
31+
QuerydslProjector(EntityManager entityManager) {
32+
super(entityManager);
33+
}
34+
35+
@Override
36+
void applyEntityGraph(AbstractJPAQuery<?, ?> query, EntityGraph<?> entityGraph) {
37+
query.setHint("javax.persistence.fetchgraph", entityGraph);
38+
}
39+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2021 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.support;
17+
18+
import javax.persistence.EntityGraph;
19+
import javax.persistence.EntityManager;
20+
import javax.persistence.TypedQuery;
21+
22+
/**
23+
* Applies fetchgraph hints to {@code TypedQuery}.
24+
*
25+
* @author Jens Schauder
26+
* @since 2.6
27+
*/
28+
public class TypedQueryProjector extends Projector<TypedQuery<?>> {
29+
30+
public TypedQueryProjector(EntityManager entityManager) {
31+
super(entityManager);
32+
}
33+
34+
void applyEntityGraph(TypedQuery<?> query, EntityGraph<?> entityGraph) {
35+
query.setHint("javax.persistence.fetchgraph", entityGraph);
36+
}
37+
}

‎src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import javax.persistence.criteria.Root;
4646

4747
import org.assertj.core.api.SoftAssertions;
48+
import org.hibernate.LazyInitializationException;
4849
import org.junit.jupiter.api.BeforeEach;
4950
import org.junit.jupiter.api.Disabled;
5051
import org.junit.jupiter.api.Test;
@@ -2138,6 +2139,84 @@ void findByFluentExampleWithInterfaceBasedProjection() {
21382139
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
21392140
}
21402141

2142+
@Test // GH-2294
2143+
void findByFluentExampleWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
2144+
2145+
flushTestUsers();
2146+
// make sure we don't get preinitialized entities back:
2147+
em.clear();
2148+
2149+
User prototype = new User();
2150+
prototype.setFirstname("v");
2151+
2152+
List<User> users = repository.findBy(
2153+
of(prototype,
2154+
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
2155+
GenericPropertyMatcher::contains)), //
2156+
q -> q.project("firstname").all());
2157+
2158+
// remove the entities, so lazy loading throws an exception
2159+
em.clear();
2160+
2161+
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
2162+
thirdUser.getFirstname(), fourthUser.getFirstname());
2163+
2164+
assertThatExceptionOfType(LazyInitializationException.class) //
2165+
.isThrownBy( //
2166+
() -> users.forEach(u -> u.getRoles().size()) // forces loading of roles
2167+
);
2168+
}
2169+
2170+
@Test // GH-2294
2171+
void findByFluentExampleWithCollectionPropertyPathsDoesntLoadUnrequestedPaths() {
2172+
2173+
flushTestUsers();
2174+
// make sure we don't get preinitialized entities back:
2175+
em.clear();
2176+
2177+
User prototype = new User();
2178+
prototype.setFirstname("v");
2179+
2180+
List<User> users = repository.findBy(
2181+
of(prototype,
2182+
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
2183+
GenericPropertyMatcher::contains)), //
2184+
q -> q.project("firstname", "roles").all());
2185+
2186+
// remove the entities, so lazy loading throws an exception
2187+
em.clear();
2188+
2189+
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
2190+
thirdUser.getFirstname(), fourthUser.getFirstname());
2191+
2192+
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
2193+
}
2194+
2195+
@Test // GH-2294
2196+
void findByFluentExampleWithComplexPropertyPathsDoesntLoadUnrequestedPaths() {
2197+
2198+
flushTestUsers();
2199+
// make sure we don't get preinitialized entities back:
2200+
em.clear();
2201+
2202+
User prototype = new User();
2203+
prototype.setFirstname("v");
2204+
2205+
List<User> users = repository.findBy(
2206+
of(prototype,
2207+
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
2208+
GenericPropertyMatcher::contains)), //
2209+
q -> q.project("roles.name").all());
2210+
2211+
// remove the entities, so lazy loading throws an exception
2212+
em.clear();
2213+
2214+
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
2215+
thirdUser.getFirstname(), fourthUser.getFirstname());
2216+
2217+
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
2218+
}
2219+
21412220
@Test // GH-2294
21422221
void findByFluentExampleWithSortedInterfaceBasedProjection() {
21432222

‎src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
import java.sql.Date;
2323
import java.time.LocalDate;
2424
import java.util.List;
25+
import java.util.Set;
2526
import java.util.stream.Stream;
2627

2728
import javax.persistence.EntityManager;
2829
import javax.persistence.PersistenceContext;
2930

31+
import org.hibernate.LazyInitializationException;
3032
import org.junit.jupiter.api.BeforeEach;
3133
import org.junit.jupiter.api.Test;
3234
import org.junit.jupiter.api.extension.ExtendWith;
@@ -405,8 +407,12 @@ void findByFluentPredicateWithInterfaceBasedProjection() {
405407
@Test // GH-2294
406408
void findByFluentPredicateWithSortedInterfaceBasedProjection() {
407409

408-
List<UserProjectionInterfaceBased> userProjections = predicateExecutor.findBy(user.firstname.contains("v"),
409-
q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all());
410+
List<UserProjectionInterfaceBased> userProjections = predicateExecutor.findBy( //
411+
user.firstname.contains("v"), //
412+
q -> q.as(UserProjectionInterfaceBased.class) //
413+
.sortBy(Sort.by("firstname")) //
414+
.all() //
415+
);
410416

411417
assertThat(userProjections).extracting(UserProjectionInterfaceBased::getFirstname)
412418
.containsExactly(dave.getFirstname(), oliver.getFirstname());
@@ -442,7 +448,79 @@ class UserDto {
442448
.findBy(user.firstname.contains("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all()));
443449
}
444450

451+
@Test // GH-2329
452+
void findByFluentPredicateWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
453+
454+
// make sure the entities are actually written to the database:
455+
em.flush();
456+
// make sure we don't get preinitialized entities back:
457+
em.clear();
458+
459+
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"),
460+
q -> q.project("firstname", "lastname").all());
461+
462+
// remove the entities, so lazy loading throws an exception
463+
em.clear();
464+
465+
assertThat(users).extracting(User::getFirstname) //
466+
.containsExactlyInAnyOrder( //
467+
dave.getFirstname(), //
468+
oliver.getFirstname() //
469+
);
470+
471+
assertThatExceptionOfType(LazyInitializationException.class) //
472+
.isThrownBy( //
473+
() -> users.forEach(u -> u.getRoles().size()) // forces loading of roles
474+
);
475+
}
476+
477+
@Test // GH-2329
478+
void findByFluentPredicateWithCollectionPropertyPathsLoadsRequestedPaths() {
479+
480+
// make sure the entities are actually written to the database:
481+
em.flush();
482+
// make sure we don't get preinitialized entities back:
483+
em.clear();
484+
485+
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"),
486+
q -> q.project("firstname", "roles").all());
487+
488+
// remove the entities, so lazy loading throws an exception
489+
em.clear();
490+
491+
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder( //
492+
dave.getFirstname(), //
493+
oliver.getFirstname() //
494+
);
495+
496+
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
497+
498+
}
499+
500+
@Test // GH-2329
501+
void findByFluentPredicateWithComplexPropertyPathsDoesntLoadsRequestedPaths() {
502+
503+
// make sure the entities are actually written to the database:
504+
em.flush();
505+
// make sure we don't get preinitialized entities back:
506+
em.clear();
507+
508+
List<User> users = predicateExecutor.findBy(user.firstname.contains("v"), q -> q.project("roles.name").all());
509+
510+
// remove the entities, so lazy loading throws an exception
511+
em.clear();
512+
513+
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder( //
514+
dave.getFirstname(), //
515+
oliver.getFirstname() //
516+
);
517+
518+
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
519+
}
520+
445521
private interface UserProjectionInterfaceBased {
446522
String getFirstname();
523+
524+
Set<Role> getRoles();
447525
}
448526
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2021 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.support;
17+
18+
import static java.util.Arrays.*;
19+
import static java.util.Collections.*;
20+
import static org.mockito.Mockito.*;
21+
22+
import java.util.HashSet;
23+
24+
import javax.persistence.EntityGraph;
25+
import javax.persistence.EntityManager;
26+
import javax.persistence.Subgraph;
27+
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
31+
import com.querydsl.jpa.impl.AbstractJPAQuery;
32+
33+
/**
34+
* Unit tests for {@link QuerydslProjector}.
35+
*
36+
* @author Jens Schauder
37+
*/
38+
public class QuerydslProjectorUnitTests {
39+
40+
EntityManager em = mock(EntityManager.class);
41+
private EntityGraph entityGraph;
42+
private AbstractJPAQuery jpaQuery = mock(AbstractJPAQuery.class);
43+
44+
@BeforeEach
45+
void beforeEach() {
46+
47+
entityGraph = mock(EntityGraph.class, RETURNS_DEEP_STUBS);
48+
when(em.createEntityGraph(DummyEntity.class)).thenReturn(entityGraph);
49+
}
50+
51+
// GH-2329
52+
@Test
53+
void emptySetOfPropertiesDoesNotCreateEntityGraph() {
54+
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, emptySet());
55+
}
56+
57+
// GH-2329
58+
@Test
59+
void simpleSetOfPropertiesGetRegistered() {
60+
61+
final HashSet<String> properties = new HashSet<>(asList("one", "two"));
62+
63+
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties);
64+
65+
verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph);
66+
verify(entityGraph).addAttributeNodes("one");
67+
verify(entityGraph).addAttributeNodes("two");
68+
}
69+
70+
// GH-2329
71+
@Test
72+
void setOfCompositePropertiesGetRegisteredPiecewise() {
73+
74+
final HashSet<String> properties = new HashSet<>(asList("one.two", "eins.zwei.drei"));
75+
76+
new QuerydslProjector(em).apply(DummyEntity.class, jpaQuery, properties);
77+
78+
verify(jpaQuery).setHint("javax.persistence.fetchgraph", entityGraph);
79+
80+
verify(entityGraph).addSubgraph("one");
81+
Subgraph one = entityGraph.addSubgraph("one");
82+
verify(one).addAttributeNodes("two");
83+
84+
verify(entityGraph).addSubgraph("eins");
85+
Subgraph eins = entityGraph.addSubgraph("eins");
86+
verify(eins).addSubgraph("zwei");
87+
Subgraph zwei = eins.addSubgraph("zwei");
88+
verify(zwei).addAttributeNodes("drei");
89+
}
90+
91+
private static class DummyEntity {
92+
DummyEntity one;
93+
DummyEntity two;
94+
DummyEntity eins;
95+
DummyEntity zwei;
96+
DummyEntity drei;
97+
}
98+
}

0 commit comments

Comments
 (0)
Please sign in to comment.