Skip to content

Commit c5129ac

Browse files
christophstroblmp911de
authored andcommitted
DATAMONGO-1979 - Add default sorting for repository query methods using @query(sort = "…").
We now allow to set a default sort for repository query methods via the @query annotation. @query(sort = "{ age : -1 }") List<Person> findByFirstname(String firstname); Using an explicit Sort parameter along with the annotated one allows to alter the defaults set via the annotation. Method argument sort parameters add to / override the annotated defaults. @query(sort = "{ age : -1 }") List<Person> findByFirstname(String firstname, Sort sort); Original pull request: #566.
1 parent dfede78 commit c5129ac

File tree

10 files changed

+257
-2
lines changed

10 files changed

+257
-2
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,22 @@
7777
* @return
7878
*/
7979
boolean delete() default false;
80+
81+
/**
82+
* Defines a default sort order for the given query.<br />
83+
* <strong>NOTE</strong> The so set defaults can be altered / overwritten via an explicit
84+
* {@link org.springframework.data.domain.Sort} argument of the query method.
85+
*
86+
* <pre>
87+
* <code>
88+
*
89+
* &#64;Query(sort = "{ age : -1 }") // order by age descending
90+
* List<Person> findByFirstname(String firstname);
91+
* </code>
92+
* </pre>
93+
*
94+
* @return
95+
* @since 2.1
96+
*/
97+
String sort() default "";
8098
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import org.bson.Document;
1819
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
1920
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
2021
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
@@ -84,6 +85,7 @@ public Object execute(Object[] parameters) {
8485
Query query = createQuery(accessor);
8586

8687
applyQueryMetaAttributesWhenPresent(query);
88+
query = applyAnnotatedDefaultSortIfPresent(query);
8789

8890
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
8991
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -110,7 +112,7 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
110112
} else if (method.isStreamQuery()) {
111113
return q -> operation.matching(q).stream();
112114
} else if (method.isCollectionQuery()) {
113-
return q -> operation.matching(q.with(accessor.getPageable())).all();
115+
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
114116
} else if (method.isPageQuery()) {
115117
return new PagedExecution(operation, accessor.getPageable());
116118
} else if (isCountQuery()) {
@@ -135,6 +137,23 @@ Query applyQueryMetaAttributesWhenPresent(Query query) {
135137
return query;
136138
}
137139

140+
/**
141+
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
142+
* {@link Query} if present.
143+
*
144+
* @param query the {@link Query} to potentially apply the sort to.
145+
* @return the query with potential default sort applied.
146+
* @since 2.1
147+
*/
148+
Query applyAnnotatedDefaultSortIfPresent(Query query) {
149+
150+
if (!method.hasAnnotatedSort()) {
151+
return query;
152+
}
153+
154+
return QueryUtils.sneakInDefaultSort(query, Document.parse(method.getAnnotatedSort()));
155+
}
156+
138157
/**
139158
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
140159
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import org.bson.Document;
1819
import reactor.core.publisher.Flux;
1920
import reactor.core.publisher.Mono;
2021

@@ -109,6 +110,7 @@ private Object execute(MongoParameterAccessor parameterAccessor) {
109110
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor));
110111

111112
applyQueryMetaAttributesWhenPresent(query);
113+
query = applyAnnotatedDefaultSortIfPresent(query);
112114

113115
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
114116
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
@@ -177,6 +179,23 @@ Query applyQueryMetaAttributesWhenPresent(Query query) {
177179
return query;
178180
}
179181

182+
/**
183+
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
184+
* {@link Query} if present.
185+
*
186+
* @param query the {@link Query} to potentially apply the sort to.
187+
* @return the query with potential default sort applied.
188+
* @since 2.1
189+
*/
190+
Query applyAnnotatedDefaultSortIfPresent(Query query) {
191+
192+
if (!method.hasAnnotatedSort()) {
193+
return query;
194+
}
195+
196+
return QueryUtils.sneakInDefaultSort(query, Document.parse(method.getAnnotatedSort()));
197+
}
198+
180199
/**
181200
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
182201
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package org.springframework.data.mongodb.repository.query;
1717

1818
import java.io.Serializable;
19+
import java.lang.annotation.Annotation;
1920
import java.lang.reflect.Method;
2021
import java.util.Arrays;
2122
import java.util.List;
23+
import java.util.Map;
2224
import java.util.Optional;
2325

2426
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -40,6 +42,7 @@
4042
import org.springframework.lang.Nullable;
4143
import org.springframework.util.Assert;
4244
import org.springframework.util.ClassUtils;
45+
import org.springframework.util.ConcurrentReferenceHashMap;
4346
import org.springframework.util.ObjectUtils;
4447
import org.springframework.util.StringUtils;
4548

@@ -57,6 +60,7 @@ public class MongoQueryMethod extends QueryMethod {
5760

5861
private final Method method;
5962
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
63+
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
6064

6165
private @Nullable MongoEntityMetadata<?> metadata;
6266

@@ -77,6 +81,7 @@ public MongoQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa
7781

7882
this.method = method;
7983
this.mappingContext = mappingContext;
84+
this.annotationCache = new ConcurrentReferenceHashMap();
8085
}
8186

8287
/*
@@ -283,4 +288,35 @@ public org.springframework.data.mongodb.core.query.Meta getQueryMetaAttributes()
283288

284289
return metaAttributes;
285290
}
291+
292+
/**
293+
* Check if the query method is decorated with an non empty {@link Query#sort()}.
294+
*
295+
* @return true if method annotated with {@link Query} having an non empty sort attribute.
296+
* @since 2.1
297+
*/
298+
public boolean hasAnnotatedSort() {
299+
return doFindAnnotation(Query.class).map(it -> !it.sort().isEmpty()).orElse(false);
300+
}
301+
302+
/**
303+
* Get the sort value, used as default, extracted from the {@link Query} annotation.
304+
*
305+
* @return the {@link Query#sort()} value.
306+
* @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
307+
* {@link #hasAnnotatedQuery()} first.
308+
* @since 2.1
309+
*/
310+
public String getAnnotatedSort() {
311+
312+
return doFindAnnotation(Query.class).map(Query::sort).orElseThrow(() -> new IllegalStateException(
313+
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before."));
314+
}
315+
316+
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
317+
318+
return (Optional) this.annotationCache.computeIfAbsent(annotationType, (it) -> {
319+
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it));
320+
});
321+
}
286322
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.mongodb.repository.query;
17+
18+
import org.aopalliance.intercept.MethodInterceptor;
19+
import org.bson.Document;
20+
import org.springframework.aop.framework.ProxyFactory;
21+
import org.springframework.data.mongodb.core.query.Query;
22+
23+
/**
24+
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
25+
* offered by repositories.
26+
*
27+
* @author Christoph Strobl
28+
* @since 2.1
29+
* @currentRead Assassin's Apprentice - Robin Hobb
30+
*/
31+
class QueryUtils {
32+
33+
/**
34+
* Add a default sort expression to the given Query. Attributes of the given {@code sort} may be overwritten by the
35+
* sort explicitly defined by the {@link Query} itself.
36+
*
37+
* @param query the {@link Query} to decorate.
38+
* @param defaultSort the default sort expression to apply to the query.
39+
* @return the query having the given {@code sort} applied.
40+
*/
41+
static Query sneakInDefaultSort(Query query, Document defaultSort) {
42+
43+
if (defaultSort.isEmpty()) {
44+
return query;
45+
}
46+
47+
ProxyFactory factory = new ProxyFactory(query);
48+
factory.addAdvice((MethodInterceptor) invocation -> {
49+
50+
if (!invocation.getMethod().getName().equals("getSortObject")) {
51+
return invocation.proceed();
52+
}
53+
54+
Document combinedSort = new Document(defaultSort);
55+
combinedSort.putAll((Document) invocation.proceed());
56+
return combinedSort;
57+
});
58+
59+
return (Query) factory.getProxy();
60+
}
61+
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,4 +1205,15 @@ public void findSingleEntityThrowsErrorWhenNotUnique() {
12051205
public void findOptionalSingleEntityThrowsErrorWhenNotUnique() {
12061206
repository.findOptionalPersonByLastnameLike(dave.getLastname());
12071207
}
1208+
1209+
@Test // DATAMONGO-1979
1210+
public void findAppliesAnnotatedSort() {
1211+
assertThat(repository.findByAgeGreaterThan(40)).containsExactly(carter, boyd, dave, leroi);
1212+
}
1213+
1214+
@Test // DATAMONGO-1979
1215+
public void findWithSortOverwritesAnnotatedSort() {
1216+
assertThat(repository.findByAgeGreaterThan(40, Sort.by(Direction.ASC, "age"))).containsExactly(leroi, dave, boyd,
1217+
carter);
1218+
}
12081219
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,10 @@ Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li
347347

348348
// DATAMONGO-1752
349349
Iterable<PersonSummary> findClosedProjectionBy();
350+
351+
@Query(sort = "{ age : -1 }")
352+
List<Person> findByAgeGreaterThan(int age);
353+
354+
@Query(sort = "{ age : -1 }")
355+
List<Person> findByAgeGreaterThan(int age, Sort sort);
350356
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static org.springframework.data.domain.Sort.Direction.*;
2121

2222
import lombok.NoArgsConstructor;
23+
import org.hamcrest.collection.IsIterableContainingInOrder;
24+
import org.springframework.data.domain.Sort.Direction;
2325
import reactor.core.Disposable;
2426
import reactor.core.publisher.Flux;
2527
import reactor.core.publisher.Mono;
@@ -307,6 +309,23 @@ public void shouldReturnFirstFindFirstWithMoreResults() {
307309
StepVerifier.create(repository.findFirstByLastname(dave.getLastname())).expectNextCount(1).verifyComplete();
308310
}
309311

312+
@Test // DATAMONGO-1979
313+
public void findAppliesAnnotatedSort() {
314+
315+
repository.findByAgeGreaterThan(40).collectList().as(StepVerifier::create).consumeNextWith(result -> {
316+
assertThat(result, IsIterableContainingInOrder.contains(carter, boyd, dave, leroi));
317+
});
318+
}
319+
320+
@Test // DATAMONGO-1979
321+
public void findWithSortOverwritesAnnotatedSort() {
322+
323+
repository.findByAgeGreaterThan(40, Sort.by(Direction.ASC, "age")).collectList().as(StepVerifier::create)
324+
.consumeNextWith(result -> {
325+
assertThat(result, IsIterableContainingInOrder.contains(leroi, dave, boyd, carter));
326+
});
327+
}
328+
310329
interface ReactivePersonRepository extends ReactiveMongoRepository<Person, String> {
311330

312331
Flux<Person> findByLastname(String lastname);
@@ -335,6 +354,12 @@ interface ReactivePersonRepository extends ReactiveMongoRepository<Person, Strin
335354
Flux<Person> findPersonByLocationNear(Point point, Distance maxDistance);
336355

337356
Mono<Person> findFirstByLastname(String lastname);
357+
358+
@Query(sort = "{ age : -1 }")
359+
Flux<Person> findByAgeGreaterThan(int age);
360+
361+
@Query(sort = "{ age : -1 }")
362+
Flux<Person> findByAgeGreaterThan(int age, Sort sort);
338363
}
339364

340365
interface ReactiveCappedCollectionRepository extends Repository<Capped, String> {

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
20-
import static org.mockito.ArgumentMatchers.*;
2120
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.ArgumentMatchers.anyString;
22+
import static org.mockito.ArgumentMatchers.eq;
2223
import static org.mockito.Mockito.*;
2324

2425
import java.lang.reflect.Method;
@@ -39,6 +40,7 @@
3940
import org.springframework.data.domain.Pageable;
4041
import org.springframework.data.domain.Slice;
4142
import org.springframework.data.domain.Sort;
43+
import org.springframework.data.domain.Sort.Direction;
4244
import org.springframework.data.mongodb.MongoDbFactory;
4345
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
4446
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
@@ -284,6 +286,28 @@ public void doesNotFixCollectionOnPreparation() {
284286
verify(executableFind).as(DynamicallyMapped.class);
285287
}
286288

289+
@Test // DATAMONGO-1979
290+
public void usesAnnotatedSortWhenPresent() {
291+
292+
createQueryForMethod("findByAge", Integer.class) //
293+
.execute(new Object[] { 1000 });
294+
295+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
296+
verify(withQueryMock).matching(captor.capture());
297+
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", 1))));
298+
}
299+
300+
@Test // DATAMONGO-1979
301+
public void usesExplicitSortOverridesAnnotatedSortWhenPresent() {
302+
303+
createQueryForMethod("findByAge", Integer.class, Sort.class) //
304+
.execute(new Object[] { 1000, Sort.by(Direction.DESC, "age") });
305+
306+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
307+
verify(withQueryMock).matching(captor.capture());
308+
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", -1))));
309+
}
310+
287311
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
288312
return createQueryForMethod(Repo.class, methodName, paramTypes);
289313
}
@@ -370,6 +394,12 @@ private interface Repo extends MongoRepository<Person, Long> {
370394
Optional<Person> findByLastname(String lastname);
371395

372396
Person findFirstByLastname(String lastname);
397+
398+
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
399+
List<Person> findByAge(Integer age);
400+
401+
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
402+
List<Person> findByAge(Integer age, Sort page);
373403
}
374404

375405
// DATAMONGO-1872

0 commit comments

Comments
 (0)