Skip to content

Commit 18c775a

Browse files
committed
Add fluent findBy API to JpaSpecificationExecutor.
Extend fluent findBy support through the usage of Specifications. See #2274.
1 parent 66aaa25 commit 18c775a

File tree

5 files changed

+415
-19
lines changed

5 files changed

+415
-19
lines changed

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
*/
1616
package org.springframework.data.jpa.domain;
1717

18-
import java.io.Serializable;
19-
2018
import jakarta.persistence.criteria.CriteriaBuilder;
2119
import jakarta.persistence.criteria.CriteriaQuery;
2220
import jakarta.persistence.criteria.Predicate;
2321
import jakarta.persistence.criteria.Root;
2422

23+
import java.io.Serializable;
24+
2525
import org.springframework.lang.Nullable;
2626

2727
/**

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java

+14
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
import java.util.List;
1919
import java.util.Optional;
20+
import java.util.function.Function;
2021

2122
import org.springframework.data.domain.Page;
2223
import org.springframework.data.domain.Pageable;
2324
import org.springframework.data.domain.Sort;
2425
import org.springframework.data.jpa.domain.Specification;
26+
import org.springframework.data.repository.query.FluentQuery;
2527
import org.springframework.lang.Nullable;
2628

2729
/**
@@ -84,4 +86,16 @@ public interface JpaSpecificationExecutor<T> {
8486
* <code>false</code>.
8587
*/
8688
boolean exists(Specification<T> spec);
89+
90+
/**
91+
* Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query
92+
* and its result type.
93+
*
94+
* @param example – must not be null.
95+
* @param queryFunction – the query function defining projection, sorting, and the result type
96+
* @return all entities matching the given Example.
97+
* @since 3.0
98+
*/
99+
<S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
100+
87101
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2021-2022 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 jakarta.persistence.EntityManager;
19+
import jakarta.persistence.TypedQuery;
20+
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.function.Function;
26+
import java.util.stream.Stream;
27+
28+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
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.domain.Specification;
34+
import org.springframework.data.repository.query.FluentQuery;
35+
import org.springframework.data.support.PageableExecutionUtils;
36+
import org.springframework.util.Assert;
37+
38+
/**
39+
* Immutable implementation of {@link FetchableFluentQuery} based on a {@link Specification}. All methods that return a
40+
* {@link FetchableFluentQuery} will return a new instance, not the original.
41+
*
42+
* @param <S> Domain type
43+
* @param <R> Result type
44+
* @author Greg Turnquist
45+
* @since 3.0
46+
*/
47+
class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
48+
implements FluentQuery.FetchableFluentQuery<R> {
49+
50+
private final Specification<S> spec;
51+
private final Function<Sort, TypedQuery<S>> finder;
52+
private final Function<Specification<S>, Long> countOperation;
53+
private final Function<Specification<S>, Boolean> existsOperation;
54+
private final EntityManager entityManager;
55+
56+
public FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Sort sort,
57+
Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
58+
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
59+
EntityManager entityManager) {
60+
this(spec, entityType, (Class<R>) entityType, Sort.unsorted(), Collections.emptySet(), finder, countOperation,
61+
existsOperation, entityManager);
62+
}
63+
64+
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
65+
Sort sort, Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
66+
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
67+
EntityManager entityManager) {
68+
69+
super(resultType, sort, properties, entityType);
70+
this.spec = spec;
71+
this.finder = finder;
72+
this.countOperation = countOperation;
73+
this.existsOperation = existsOperation;
74+
this.entityManager = entityManager;
75+
}
76+
77+
@Override
78+
public FetchableFluentQuery<R> sortBy(Sort sort) {
79+
80+
Assert.notNull(sort, "Sort must not be null!");
81+
82+
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), properties,
83+
finder, countOperation, existsOperation, entityManager);
84+
}
85+
86+
@Override
87+
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
88+
89+
Assert.notNull(resultType, "Projection target type must not be null!");
90+
if (!resultType.isInterface()) {
91+
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
92+
}
93+
94+
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
95+
countOperation, existsOperation, entityManager);
96+
}
97+
98+
@Override
99+
public FetchableFluentQuery<R> project(Collection<String> properties) {
100+
101+
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
102+
countOperation, existsOperation, entityManager);
103+
}
104+
105+
@Override
106+
public R oneValue() {
107+
108+
List<?> results = createSortedAndProjectedQuery() //
109+
.setMaxResults(2) // Never need more than 2 values
110+
.getResultList();
111+
112+
if (results.size() > 1) {
113+
throw new IncorrectResultSizeDataAccessException(1);
114+
}
115+
116+
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
117+
}
118+
119+
@Override
120+
public R firstValue() {
121+
122+
List<?> results = createSortedAndProjectedQuery() //
123+
.setMaxResults(1) // Never need more than 1 value
124+
.getResultList();
125+
126+
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
127+
}
128+
129+
@Override
130+
public List<R> all() {
131+
return convert(createSortedAndProjectedQuery().getResultList());
132+
}
133+
134+
@Override
135+
public Page<R> page(Pageable pageable) {
136+
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
137+
}
138+
139+
@Override
140+
public Stream<R> stream() {
141+
142+
return createSortedAndProjectedQuery() //
143+
.getResultStream() //
144+
.map(getConversionFunction());
145+
}
146+
147+
@Override
148+
public long count() {
149+
return countOperation.apply(spec);
150+
}
151+
152+
@Override
153+
public boolean exists() {
154+
return existsOperation.apply(spec);
155+
}
156+
157+
private TypedQuery<S> createSortedAndProjectedQuery() {
158+
159+
TypedQuery<S> query = finder.apply(sort);
160+
161+
if (!properties.isEmpty()) {
162+
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
163+
}
164+
165+
return query;
166+
}
167+
168+
private Page<R> readPage(Pageable pageable) {
169+
170+
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();
171+
172+
if (pageable.isPaged()) {
173+
pagedQuery.setFirstResult((int) pageable.getOffset());
174+
pagedQuery.setMaxResults(pageable.getPageSize());
175+
}
176+
177+
List<R> paginatedResults = convert(pagedQuery.getResultList());
178+
179+
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(spec));
180+
}
181+
182+
private List<R> convert(List<S> resultList) {
183+
184+
Function<Object, R> conversionFunction = getConversionFunction();
185+
List<R> mapped = new ArrayList<>(resultList.size());
186+
187+
for (S s : resultList) {
188+
mapped.add(conversionFunction.apply(s));
189+
}
190+
return mapped;
191+
}
192+
193+
private Function<Object, R> getConversionFunction() {
194+
return getConversionFunction(entityType, resultType);
195+
}
196+
}

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

+25-9
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@
1717

1818
import static org.springframework.data.jpa.repository.query.QueryUtils.*;
1919

20-
import java.util.ArrayList;
21-
import java.util.Collection;
22-
import java.util.Collections;
23-
import java.util.HashMap;
24-
import java.util.List;
25-
import java.util.Map;
26-
import java.util.Optional;
27-
import java.util.function.Function;
28-
2920
import jakarta.persistence.EntityManager;
3021
import jakarta.persistence.LockModeType;
3122
import jakarta.persistence.NoResultException;
@@ -39,6 +30,15 @@
3930
import jakarta.persistence.criteria.Predicate;
4031
import jakarta.persistence.criteria.Root;
4132

33+
import java.util.ArrayList;
34+
import java.util.Collection;
35+
import java.util.Collections;
36+
import java.util.HashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Optional;
40+
import java.util.function.Function;
41+
4242
import org.springframework.dao.EmptyResultDataAccessException;
4343
import org.springframework.data.domain.Example;
4444
import org.springframework.data.domain.Page;
@@ -515,6 +515,22 @@ public <S extends T, R> R findBy(Example<S> example, Function<FetchableFluentQue
515515
return queryFunction.apply(fluentQuery);
516516
}
517517

518+
@Override
519+
public <S extends T, R> R findBy(Specification<T> spec, Function<FetchableFluentQuery<S>, R> queryFunction) {
520+
521+
Assert.notNull(spec, "Specification must not be null!");
522+
Assert.notNull(queryFunction, "Query function must not be null!");
523+
524+
Function<Sort, TypedQuery<T>> finder = sort -> {
525+
return getQuery(spec, getDomainClass(), sort);
526+
};
527+
528+
FetchableFluentQuery<R> fluentQuery = new FetchableFluentQueryBySpecification<T, R>(spec, getDomainClass(),
529+
Sort.unsorted(), null, finder, this::count, this::exists, this.em);
530+
531+
return queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
532+
}
533+
518534
@Override
519535
public long count() {
520536
return em.createQuery(getCountQueryString(), Long.class).getSingleResult();

0 commit comments

Comments
 (0)