Skip to content

Commit 64beaab

Browse files
committed
Implement FluentQuery for Querydsl and Query by Example.
Add support for both QueryByExampleExecutor and QuerydslPredicateExecutor. This is used in SimpleJpaRepository and QuerydslJpaPredicateExecutor, resulting in various test cases proving support by both examples and Querydsl predicates. NOTE: Class-based DTOs are NOT supported yet. Closes #2294. Related: #2327.
1 parent 8592dec commit 64beaab

12 files changed

+875
-28
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<hibernate>5.5.3.Final</hibernate>
2626
<mysql-connector-java>8.0.23</mysql-connector-java>
2727
<postgresql>42.2.19</postgresql>
28-
<springdata.commons>2.6.0-SNAPSHOT</springdata.commons>
28+
<springdata.commons>2.6.0-2228-SNAPSHOT</springdata.commons>
2929
<vavr>0.10.3</vavr>
3030

3131
<hibernate.groupId>org.hibernate</hibernate.groupId>

src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ protected Predicate or(Predicate base, Predicate predicate) {
144144

145145
/**
146146
* Finalizes the given {@link Predicate} and applies the given sort. Delegates to
147-
* {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current {@link CriteriaQuery}
148-
* and {@link CriteriaBuilder}.
147+
* {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current
148+
* {@link CriteriaQuery} and {@link CriteriaBuilder}.
149149
*/
150150
@Override
151151
protected final CriteriaQuery<? extends Object> complete(Predicate predicate, Sort sort) {
@@ -271,10 +271,12 @@ public Predicate build() {
271271
return getTypedPath(root, part).isNotNull();
272272
case NOT_IN:
273273
// cast required for eclipselink workaround, see DATAJPA-433
274-
return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
274+
return upperIfIgnoreCase(getTypedPath(root, part))
275+
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
275276
case IN:
276277
// cast required for eclipselink workaround, see DATAJPA-433
277-
return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
278+
return upperIfIgnoreCase(getTypedPath(root, part))
279+
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
278280
case STARTING_WITH:
279281
case ENDING_WITH:
280282
case CONTAINING:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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.Collection;
19+
import java.util.List;
20+
import java.util.function.Function;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
23+
24+
import javax.persistence.EntityManager;
25+
import javax.persistence.TypedQuery;
26+
27+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
28+
import org.springframework.data.domain.Example;
29+
import org.springframework.data.domain.Page;
30+
import org.springframework.data.domain.PageImpl;
31+
import org.springframework.data.domain.Pageable;
32+
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.jpa.repository.query.EscapeCharacter;
34+
import org.springframework.data.mapping.PersistentEntity;
35+
import org.springframework.data.mapping.PersistentProperty;
36+
import org.springframework.data.mapping.context.MappingContext;
37+
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
38+
import org.springframework.data.support.PageableExecutionUtils;
39+
import org.springframework.lang.Nullable;
40+
41+
/**
42+
* Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a
43+
* {@link FetchableFluentQuery} will return a new instance, not the original.
44+
*
45+
* @param <S> Domain type
46+
* @param <R> Result type
47+
* @author Greg Turnquist
48+
* @since 2.6
49+
*/
50+
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
51+
52+
private final Example<S> example;
53+
private final Function<Sort, TypedQuery<S>> finder;
54+
private final Function<Example<S>, Long> countOperation;
55+
private final Function<Example<S>, Boolean> existsOperation;
56+
private final EntityManager entityManager;
57+
private final EscapeCharacter escapeCharacter;
58+
59+
public FetchableFluentQueryByExample(Example<S> example, Function<Sort, TypedQuery<S>> finder,
60+
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
61+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
62+
EntityManager entityManager, EscapeCharacter escapeCharacter) {
63+
this(example, (Class<R>) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation,
64+
context, entityManager, escapeCharacter);
65+
}
66+
67+
private FetchableFluentQueryByExample(Example<S> example, Class<R> returnType, Sort sort,
68+
@Nullable Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
69+
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation,
70+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
71+
EntityManager entityManager, EscapeCharacter escapeCharacter) {
72+
73+
super(returnType, sort, properties, context);
74+
this.example = example;
75+
this.finder = finder;
76+
this.countOperation = countOperation;
77+
this.existsOperation = existsOperation;
78+
this.entityManager = entityManager;
79+
this.escapeCharacter = escapeCharacter;
80+
}
81+
82+
@Override
83+
public FetchableFluentQuery<R> sortBy(Sort sort) {
84+
85+
return new FetchableFluentQueryByExample<S, R>(this.example, this.resultType, this.sort.and(sort), this.properties,
86+
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
87+
}
88+
89+
@Override
90+
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
91+
92+
if (!resultType.isInterface()) {
93+
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
94+
}
95+
96+
return new FetchableFluentQueryByExample<S, NR>(this.example, resultType, this.sort, this.properties, this.finder,
97+
this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
98+
}
99+
100+
@Override
101+
public FetchableFluentQuery<R> project(Collection<String> properties) {
102+
103+
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort, mergeProperties(properties),
104+
this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
105+
}
106+
107+
@Override
108+
public R oneValue() {
109+
110+
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
111+
limitedQuery.setMaxResults(2); // Never need more than 2 values
112+
113+
List<R> results = limitedQuery //
114+
.getResultStream() //
115+
.map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
116+
.collect(Collectors.toList());
117+
;
118+
119+
if (results.size() > 1) {
120+
throw new IncorrectResultSizeDataAccessException(1);
121+
}
122+
123+
return results.isEmpty() ? null : results.get(0);
124+
}
125+
126+
@Override
127+
public R firstValue() {
128+
129+
TypedQuery<S> limitedQuery = this.finder.apply(this.sort);
130+
limitedQuery.setMaxResults(1); // Never need more than 1 value
131+
132+
List<R> results = limitedQuery //
133+
.getResultStream() //
134+
.map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
135+
.collect(Collectors.toList());
136+
137+
return results.isEmpty() ? null : results.get(0);
138+
}
139+
140+
@Override
141+
public List<R> all() {
142+
return stream().collect(Collectors.toList());
143+
}
144+
145+
@Override
146+
public Page<R> page(Pageable pageable) {
147+
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
148+
}
149+
150+
@Override
151+
public Stream<R> stream() {
152+
153+
return this.finder.apply(this.sort) //
154+
.getResultStream() //
155+
.map(getConversionFunction(this.example.getProbeType(), this.resultType));
156+
}
157+
158+
@Override
159+
public long count() {
160+
return this.countOperation.apply(example);
161+
}
162+
163+
@Override
164+
public boolean exists() {
165+
return this.existsOperation.apply(example);
166+
}
167+
168+
private Page<R> readPage(Pageable pageable) {
169+
170+
TypedQuery<S> pagedQuery = this.finder.apply(this.sort);
171+
172+
if (pageable.isPaged()) {
173+
pagedQuery.setFirstResult((int) pageable.getOffset());
174+
pagedQuery.setMaxResults(pageable.getPageSize());
175+
}
176+
177+
List<R> paginatedResults = pagedQuery.getResultStream() //
178+
.map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
179+
.collect(Collectors.toList());
180+
181+
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example));
182+
}
183+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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.Collection;
19+
import java.util.List;
20+
import java.util.function.BiFunction;
21+
import java.util.function.Function;
22+
import java.util.stream.Collectors;
23+
import java.util.stream.Stream;
24+
25+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
26+
import org.springframework.data.domain.Page;
27+
import org.springframework.data.domain.PageImpl;
28+
import org.springframework.data.domain.Pageable;
29+
import org.springframework.data.domain.Sort;
30+
import org.springframework.data.mapping.PersistentEntity;
31+
import org.springframework.data.mapping.PersistentProperty;
32+
import org.springframework.data.mapping.context.MappingContext;
33+
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
34+
import org.springframework.data.support.PageableExecutionUtils;
35+
import org.springframework.lang.Nullable;
36+
37+
import com.querydsl.core.types.Predicate;
38+
import com.querydsl.jpa.JPQLQuery;
39+
40+
/**
41+
* Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that
42+
* return a {@link FetchableFluentQuery} will return a new instance, not the original.
43+
*
44+
* @param <S> Domain type
45+
* @param <R> Result type
46+
* @author Greg Turnquist
47+
* @since 2.6
48+
*/
49+
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
50+
51+
private final Predicate predicate;
52+
private final Function<Sort, JPQLQuery<S>> finder;
53+
private final BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder;
54+
private final Function<Predicate, Long> countOperation;
55+
private final Function<Predicate, Boolean> existsOperation;
56+
private final Class<S> entityType;
57+
58+
public FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Function<Sort, JPQLQuery<S>> finder,
59+
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
60+
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
61+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
62+
this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType,
63+
context);
64+
}
65+
66+
private FetchableFluentQueryByPredicate(Predicate predicate, Class<R> resultType, Sort sort,
67+
@Nullable Collection<String> properties, Function<Sort, JPQLQuery<S>> finder,
68+
BiFunction<Sort, Pageable, JPQLQuery<S>> pagedFinder, Function<Predicate, Long> countOperation,
69+
Function<Predicate, Boolean> existsOperation, Class<S> entityType,
70+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context) {
71+
72+
super(resultType, sort, properties, context);
73+
this.predicate = predicate;
74+
this.finder = finder;
75+
this.pagedFinder = pagedFinder;
76+
this.countOperation = countOperation;
77+
this.existsOperation = existsOperation;
78+
this.entityType = entityType;
79+
}
80+
81+
@Override
82+
public FetchableFluentQuery<R> sortBy(Sort sort) {
83+
84+
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort.and(sort), this.properties,
85+
this.finder, this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
86+
}
87+
88+
@Override
89+
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
90+
91+
if (!resultType.isInterface()) {
92+
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
93+
}
94+
95+
return new FetchableFluentQueryByPredicate<>(this.predicate, resultType, this.sort, this.properties, this.finder,
96+
this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
97+
}
98+
99+
@Override
100+
public FetchableFluentQuery<R> project(Collection<String> properties) {
101+
102+
return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort,
103+
mergeProperties(properties), this.finder, this.pagedFinder, this.countOperation, this.existsOperation,
104+
this.entityType, this.context);
105+
}
106+
107+
@Override
108+
public R oneValue() {
109+
110+
List<R> results = this.finder.apply(this.sort) //
111+
.limit(2) // Never need more than 2 values
112+
.stream() //
113+
.map(getConversionFunction(this.entityType, this.resultType)) //
114+
.collect(Collectors.toList());
115+
116+
if (results.size() > 1) {
117+
throw new IncorrectResultSizeDataAccessException(1);
118+
}
119+
120+
return results.isEmpty() ? null : results.get(0);
121+
}
122+
123+
@Override
124+
public R firstValue() {
125+
126+
List<R> results = this.finder.apply(this.sort) //
127+
.limit(1) // Never need more than 1 value
128+
.stream() //
129+
.map(getConversionFunction(this.entityType, this.resultType)) //
130+
.collect(Collectors.toList());
131+
132+
return results.isEmpty() ? null : results.get(0);
133+
}
134+
135+
@Override
136+
public List<R> all() {
137+
return stream().collect(Collectors.toList());
138+
}
139+
140+
@Override
141+
public Page<R> page(Pageable pageable) {
142+
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
143+
}
144+
145+
@Override
146+
public Stream<R> stream() {
147+
148+
return this.finder.apply(this.sort) //
149+
.stream() //
150+
.map(getConversionFunction(this.entityType, this.resultType));
151+
}
152+
153+
@Override
154+
public long count() {
155+
return this.countOperation.apply(this.predicate);
156+
}
157+
158+
@Override
159+
public boolean exists() {
160+
return this.existsOperation.apply(this.predicate);
161+
}
162+
163+
private Page<R> readPage(Pageable pageable) {
164+
165+
JPQLQuery<S> pagedQuery = this.pagedFinder.apply(this.sort, pageable);
166+
167+
List<R> paginatedResults = pagedQuery.stream() //
168+
.map(getConversionFunction(this.entityType, this.resultType)) //
169+
.collect(Collectors.toList());
170+
171+
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate));
172+
}
173+
}

0 commit comments

Comments
 (0)