Skip to content

Commit e1b7612

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for class-based DTOs for Fluent API.
Also, interface-based projections now use Tuple queries to consistently use tuple-based queries. Closes: #2327 Original Pull Request: #3654
1 parent 1341c3f commit e1b7612

14 files changed

+487
-158
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
305305
*/
306306
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);
307307

308-
static class TupleConverter implements Converter<Object, Object> {
308+
public static class TupleConverter implements Converter<Object, Object> {
309309

310310
private final ReturnedType type;
311311

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,8 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
769769
return toExpressionRecursively(from, property, false);
770770
}
771771

772-
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
772+
public static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property,
773+
boolean isForSelection) {
773774
return toExpressionRecursively(from, property, isForSelection, false);
774775
}
775776

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

+113-34
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@
1616
package org.springframework.data.jpa.repository.support;
1717

1818
import jakarta.persistence.EntityManager;
19-
import jakarta.persistence.Query;
2019

2120
import java.util.ArrayList;
2221
import java.util.Collection;
2322
import java.util.Collections;
23+
import java.util.LinkedHashSet;
2424
import java.util.List;
2525
import java.util.function.BiFunction;
2626
import java.util.function.Function;
2727
import java.util.stream.Stream;
2828

2929
import org.springframework.dao.IncorrectResultSizeDataAccessException;
30+
import org.springframework.data.domain.KeysetScrollPosition;
3031
import org.springframework.data.domain.Page;
3132
import org.springframework.data.domain.PageImpl;
3233
import org.springframework.data.domain.Pageable;
@@ -36,10 +37,18 @@
3637
import org.springframework.data.jpa.repository.query.ScrollDelegate;
3738
import org.springframework.data.projection.ProjectionFactory;
3839
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
40+
import org.springframework.data.repository.query.ReturnedType;
3941
import org.springframework.data.support.PageableExecutionUtils;
42+
import org.springframework.lang.Nullable;
4043
import org.springframework.util.Assert;
4144

45+
import com.querydsl.core.types.EntityPath;
46+
import com.querydsl.core.types.Expression;
47+
import com.querydsl.core.types.ExpressionBase;
4248
import com.querydsl.core.types.Predicate;
49+
import com.querydsl.core.types.Visitor;
50+
import com.querydsl.core.types.dsl.PathBuilder;
51+
import com.querydsl.jpa.JPQLSerializer;
4352
import com.querydsl.jpa.impl.AbstractJPAQuery;
4453

4554
/**
@@ -57,33 +66,41 @@
5766
*/
5867
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
5968

69+
private final EntityPath<?> entityPath;
70+
private final JpaEntityInformation<S, ?> entityInformation;
71+
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory;
6072
private final Predicate predicate;
6173
private final Function<Sort, AbstractJPAQuery<?, ?>> finder;
6274

63-
private final PredicateScrollDelegate<S> scroll;
6475
private final BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder;
6576
private final Function<Predicate, Long> countOperation;
6677
private final Function<Predicate, Boolean> existsOperation;
6778
private final EntityManager entityManager;
6879

69-
FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType,
70-
Function<Sort, AbstractJPAQuery<?, ?>> finder, PredicateScrollDelegate<S> scroll,
80+
FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
81+
JpaEntityInformation<S, ?> entityInformation, Function<Sort, AbstractJPAQuery<?, ?>> finder,
82+
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
7183
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
7284
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
73-
this(predicate, entityType, (Class<R>) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scroll,
85+
this(entityPath, predicate, entityInformation, (Class<R>) entityInformation.getJavaType(), Sort.unsorted(), 0,
86+
Collections.emptySet(), finder, scrollQueryFactory,
7487
pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
7588
}
7689

77-
private FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType, Class<R> resultType, Sort sort,
78-
int limit, Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
79-
PredicateScrollDelegate<S> scroll, BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
90+
private FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
91+
JpaEntityInformation<S, ?> entityInformation, Class<R> resultType, Sort sort, int limit,
92+
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
93+
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
94+
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
8095
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
8196
EntityManager entityManager, ProjectionFactory projectionFactory) {
8297

83-
super(resultType, sort, limit, properties, entityType, projectionFactory);
98+
super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
99+
this.entityInformation = entityInformation;
100+
this.entityPath = entityPath;
84101
this.predicate = predicate;
85102
this.finder = finder;
86-
this.scroll = scroll;
103+
this.scrollQueryFactory = scrollQueryFactory;
87104
this.pagedFinder = pagedFinder;
88105
this.countOperation = countOperation;
89106
this.existsOperation = existsOperation;
@@ -95,37 +112,37 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
95112

96113
Assert.notNull(sort, "Sort must not be null");
97114

98-
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), limit,
99-
properties, finder, scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
115+
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType,
116+
this.sort.and(sort), limit, properties, finder, scrollQueryFactory, pagedFinder, countOperation,
117+
existsOperation, entityManager, projectionFactory);
100118
}
101119

102120
@Override
103121
public FetchableFluentQuery<R> limit(int limit) {
104122

105123
Assert.isTrue(limit >= 0, "Limit must not be negative");
106124

107-
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
108-
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
125+
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
126+
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
127+
projectionFactory);
109128
}
110129

111130
@Override
112131
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
113132

114133
Assert.notNull(resultType, "Projection target type must not be null");
115134

116-
if (!resultType.isInterface()) {
117-
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
118-
}
119-
120-
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
121-
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
135+
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
136+
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
137+
projectionFactory);
122138
}
123139

124140
@Override
125141
public FetchableFluentQuery<R> project(Collection<String> properties) {
126142

127-
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit,
128-
mergeProperties(properties), finder, scroll, pagedFinder, countOperation, existsOperation, entityManager,
143+
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
144+
mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation,
145+
entityManager,
129146
projectionFactory);
130147
}
131148

@@ -163,7 +180,8 @@ public Window<R> scroll(ScrollPosition scrollPosition) {
163180

164181
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
165182

166-
return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
183+
return new PredicateScrollDelegate<>(scrollQueryFactory, entityInformation)
184+
.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
167185
}
168186

169187
@Override
@@ -192,6 +210,35 @@ public boolean exists() {
192210
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {
193211

194212
AbstractJPAQuery<?, ?> query = finder.apply(sort);
213+
applyQuerySettings(this.returnedType, this.limit, query, null);
214+
return query;
215+
}
216+
217+
private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJPAQuery<?, ?> query,
218+
@Nullable ScrollPosition scrollPosition) {
219+
220+
List<String> inputProperties = returnedType.getInputProperties();
221+
222+
if (returnedType.needsCustomConstruction() && !inputProperties.isEmpty()) {
223+
224+
Collection<String> requiredSelection;
225+
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
226+
requiredSelection = new LinkedHashSet<>(inputProperties);
227+
sort.forEach(it -> requiredSelection.add(it.getProperty()));
228+
entityInformation.getIdAttributeNames().forEach(requiredSelection::add);
229+
} else {
230+
requiredSelection = inputProperties;
231+
}
232+
233+
PathBuilder<?> builder = new PathBuilder<>(entityPath.getType(), entityPath.getMetadata());
234+
Expression<?>[] projection = requiredSelection.stream().map(builder::get).toArray(Expression[]::new);
235+
236+
if (returnedType.getReturnedType().isInterface()) {
237+
query.select(new JakartaTuple(projection));
238+
} else {
239+
query.select(new DtoProjection(returnedType.getReturnedType(), projection));
240+
}
241+
}
195242

196243
if (!properties.isEmpty()) {
197244
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
@@ -200,8 +247,6 @@ public boolean exists() {
200247
if (limit != 0) {
201248
query.limit(limit);
202249
}
203-
204-
return query;
205250
}
206251

207252
private Page<R> readPage(Pageable pageable) {
@@ -233,23 +278,57 @@ private Function<Object, R> getConversionFunction() {
233278
return getConversionFunction(entityType, resultType);
234279
}
235280

236-
static class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
281+
class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
237282

238-
private final ScrollQueryFactory scrollFunction;
283+
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollFunction;
239284

240-
PredicateScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
285+
PredicateScrollDelegate(ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
286+
JpaEntityInformation<T, ?> entity) {
241287
super(entity);
242288
this.scrollFunction = scrollQueryFactory;
243289
}
244290

245-
public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
291+
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
246292

247-
Query query = scrollFunction.createQuery(sort, scrollPosition);
248-
if (limit > 0) {
249-
query = query.setMaxResults(limit);
250-
}
251-
return scroll(query, sort, scrollPosition);
293+
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
294+
295+
applyQuerySettings(returnedType, limit, query, scrollPosition);
296+
297+
return scroll(query.createQuery(), sort, scrollPosition);
252298
}
253299
}
254300

301+
private static class DtoProjection extends ExpressionBase<Object> {
302+
303+
private final Expression<?>[] projection;
304+
305+
public DtoProjection(Class<?> resultType, Expression<?>[] projection) {
306+
super(resultType);
307+
this.projection = projection;
308+
}
309+
310+
@SuppressWarnings("unchecked")
311+
@Override
312+
public <R, C> R accept(Visitor<R, C> v, @Nullable C context) {
313+
314+
if (v instanceof JPQLSerializer s) {
315+
316+
s.append("new ").append(getType().getName()).append("(");
317+
boolean first = true;
318+
for (Expression<?> expression : projection) {
319+
if (first) {
320+
first = false;
321+
} else {
322+
s.append(", ");
323+
}
324+
325+
expression.accept(v, context);
326+
}
327+
328+
s.append(")");
329+
}
330+
331+
return (R) this;
332+
}
333+
}
255334
}

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

+13-12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Collection;
2424
import java.util.Collections;
2525
import java.util.List;
26+
import java.util.function.BiFunction;
2627
import java.util.function.Function;
2728
import java.util.stream.Stream;
2829

@@ -38,6 +39,7 @@
3839
import org.springframework.data.jpa.support.PageableUtils;
3940
import org.springframework.data.projection.ProjectionFactory;
4041
import org.springframework.data.repository.query.FluentQuery;
42+
import org.springframework.data.repository.query.ReturnedType;
4143
import org.springframework.data.support.PageableExecutionUtils;
4244
import org.springframework.util.Assert;
4345

@@ -55,13 +57,14 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
5557
implements FluentQuery.FetchableFluentQuery<R> {
5658

5759
private final Specification<S> spec;
58-
private final Function<Sort, TypedQuery<S>> finder;
60+
private final BiFunction<ReturnedType, Sort, TypedQuery<S>> finder;
5961
private final SpecificationScrollDelegate<S> scroll;
6062
private final Function<Specification<S>, Long> countOperation;
6163
private final Function<Specification<S>, Boolean> existsOperation;
6264
private final EntityManager entityManager;
6365

64-
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Function<Sort, TypedQuery<S>> finder,
66+
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType,
67+
BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
6568
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
6669
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
6770
ProjectionFactory projectionFactory) {
@@ -70,7 +73,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
7073
}
7174

7275
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
73-
Sort sort, int limit, Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
76+
Sort sort, int limit, Collection<String> properties, BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
7477
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
7578
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
7679
ProjectionFactory projectionFactory) {
@@ -106,9 +109,6 @@ public FetchableFluentQuery<R> limit(int limit) {
106109
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
107110

108111
Assert.notNull(resultType, "Projection target type must not be null");
109-
if (!resultType.isInterface()) {
110-
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
111-
}
112112

113113
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
114114
scroll, countOperation, existsOperation, entityManager, projectionFactory);
@@ -155,7 +155,7 @@ public Window<R> scroll(ScrollPosition scrollPosition) {
155155

156156
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
157157

158-
return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
158+
return scroll.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
159159
}
160160

161161
@Override
@@ -183,7 +183,7 @@ public boolean exists() {
183183

184184
private TypedQuery<S> createSortedAndProjectedQuery() {
185185

186-
TypedQuery<S> query = finder.apply(sort);
186+
TypedQuery<S> query = finder.apply(returnedType, sort);
187187

188188
if (!properties.isEmpty()) {
189189
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
@@ -227,16 +227,17 @@ private Function<Object, R> getConversionFunction() {
227227

228228
static class SpecificationScrollDelegate<T> extends ScrollDelegate<T> {
229229

230-
private final ScrollQueryFactory scrollFunction;
230+
private final ScrollQueryFactory<TypedQuery<T>> scrollFunction;
231231

232-
SpecificationScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
232+
SpecificationScrollDelegate(ScrollQueryFactory<TypedQuery<T>> scrollQueryFactory,
233+
JpaEntityInformation<T, ?> entity) {
233234
super(entity);
234235
this.scrollFunction = scrollQueryFactory;
235236
}
236237

237-
public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
238+
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
238239

239-
Query query = scrollFunction.createQuery(sort, scrollPosition);
240+
Query query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
240241

241242
if (limit > 0) {
242243
query = query.setMaxResults(limit);

0 commit comments

Comments
 (0)