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.
This commit is contained in:
committed by
Mark Paluch
parent
dfede781fb
commit
c5129aca45
@@ -77,4 +77,22 @@ public @interface Query {
|
||||
* @return
|
||||
*/
|
||||
boolean delete() default false;
|
||||
|
||||
/**
|
||||
* Defines a default sort order for the given query.<br />
|
||||
* <strong>NOTE</strong> The so set defaults can be altered / overwritten via an explicit
|
||||
* {@link org.springframework.data.domain.Sort} argument of the query method.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
*
|
||||
* @Query(sort = "{ age : -1 }") // order by age descending
|
||||
* List<Person> findByFirstname(String firstname);
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @return
|
||||
* @since 2.1
|
||||
*/
|
||||
String sort() default "";
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
|
||||
@@ -84,6 +85,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
|
||||
Query query = createQuery(accessor);
|
||||
|
||||
applyQueryMetaAttributesWhenPresent(query);
|
||||
query = applyAnnotatedDefaultSortIfPresent(query);
|
||||
|
||||
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
|
||||
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
|
||||
@@ -110,7 +112,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
|
||||
} else if (method.isStreamQuery()) {
|
||||
return q -> operation.matching(q).stream();
|
||||
} else if (method.isCollectionQuery()) {
|
||||
return q -> operation.matching(q.with(accessor.getPageable())).all();
|
||||
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
|
||||
} else if (method.isPageQuery()) {
|
||||
return new PagedExecution(operation, accessor.getPageable());
|
||||
} else if (isCountQuery()) {
|
||||
@@ -135,6 +137,23 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
|
||||
* {@link Query} if present.
|
||||
*
|
||||
* @param query the {@link Query} to potentially apply the sort to.
|
||||
* @return the query with potential default sort applied.
|
||||
* @since 2.1
|
||||
*/
|
||||
Query applyAnnotatedDefaultSortIfPresent(Query query) {
|
||||
|
||||
if (!method.hasAnnotatedSort()) {
|
||||
return query;
|
||||
}
|
||||
|
||||
return QueryUtils.sneakInDefaultSort(query, Document.parse(method.getAnnotatedSort()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
|
||||
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import org.bson.Document;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -109,6 +110,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
|
||||
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor));
|
||||
|
||||
applyQueryMetaAttributesWhenPresent(query);
|
||||
query = applyAnnotatedDefaultSortIfPresent(query);
|
||||
|
||||
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
|
||||
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
|
||||
@@ -177,6 +179,23 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default sort derived from {@link org.springframework.data.mongodb.repository.Query#sort()} to the given
|
||||
* {@link Query} if present.
|
||||
*
|
||||
* @param query the {@link Query} to potentially apply the sort to.
|
||||
* @return the query with potential default sort applied.
|
||||
* @since 2.1
|
||||
*/
|
||||
Query applyAnnotatedDefaultSortIfPresent(Query query) {
|
||||
|
||||
if (!method.hasAnnotatedSort()) {
|
||||
return query;
|
||||
}
|
||||
|
||||
return QueryUtils.sneakInDefaultSort(query, Document.parse(method.getAnnotatedSort()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
|
||||
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
@@ -40,6 +42,7 @@ import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -57,6 +60,7 @@ public class MongoQueryMethod extends QueryMethod {
|
||||
|
||||
private final Method method;
|
||||
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
|
||||
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
|
||||
|
||||
private @Nullable MongoEntityMetadata<?> metadata;
|
||||
|
||||
@@ -77,6 +81,7 @@ public class MongoQueryMethod extends QueryMethod {
|
||||
|
||||
this.method = method;
|
||||
this.mappingContext = mappingContext;
|
||||
this.annotationCache = new ConcurrentReferenceHashMap();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -283,4 +288,35 @@ public class MongoQueryMethod extends QueryMethod {
|
||||
|
||||
return metaAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the query method is decorated with an non empty {@link Query#sort()}.
|
||||
*
|
||||
* @return true if method annotated with {@link Query} having an non empty sort attribute.
|
||||
* @since 2.1
|
||||
*/
|
||||
public boolean hasAnnotatedSort() {
|
||||
return doFindAnnotation(Query.class).map(it -> !it.sort().isEmpty()).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sort value, used as default, extracted from the {@link Query} annotation.
|
||||
*
|
||||
* @return the {@link Query#sort()} value.
|
||||
* @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
|
||||
* {@link #hasAnnotatedQuery()} first.
|
||||
* @since 2.1
|
||||
*/
|
||||
public String getAnnotatedSort() {
|
||||
|
||||
return doFindAnnotation(Query.class).map(Query::sort).orElseThrow(() -> new IllegalStateException(
|
||||
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before."));
|
||||
}
|
||||
|
||||
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
|
||||
|
||||
return (Optional) this.annotationCache.computeIfAbsent(annotationType, (it) -> {
|
||||
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.bson.Document;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
|
||||
/**
|
||||
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
|
||||
* offered by repositories.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.1
|
||||
* @currentRead Assassin's Apprentice - Robin Hobb
|
||||
*/
|
||||
class QueryUtils {
|
||||
|
||||
/**
|
||||
* Add a default sort expression to the given Query. Attributes of the given {@code sort} may be overwritten by the
|
||||
* sort explicitly defined by the {@link Query} itself.
|
||||
*
|
||||
* @param query the {@link Query} to decorate.
|
||||
* @param defaultSort the default sort expression to apply to the query.
|
||||
* @return the query having the given {@code sort} applied.
|
||||
*/
|
||||
static Query sneakInDefaultSort(Query query, Document defaultSort) {
|
||||
|
||||
if (defaultSort.isEmpty()) {
|
||||
return query;
|
||||
}
|
||||
|
||||
ProxyFactory factory = new ProxyFactory(query);
|
||||
factory.addAdvice((MethodInterceptor) invocation -> {
|
||||
|
||||
if (!invocation.getMethod().getName().equals("getSortObject")) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
Document combinedSort = new Document(defaultSort);
|
||||
combinedSort.putAll((Document) invocation.proceed());
|
||||
return combinedSort;
|
||||
});
|
||||
|
||||
return (Query) factory.getProxy();
|
||||
}
|
||||
}
|
||||
@@ -1205,4 +1205,15 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
|
||||
public void findOptionalSingleEntityThrowsErrorWhenNotUnique() {
|
||||
repository.findOptionalPersonByLastnameLike(dave.getLastname());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void findAppliesAnnotatedSort() {
|
||||
assertThat(repository.findByAgeGreaterThan(40)).containsExactly(carter, boyd, dave, leroi);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void findWithSortOverwritesAnnotatedSort() {
|
||||
assertThat(repository.findByAgeGreaterThan(40, Sort.by(Direction.ASC, "age"))).containsExactly(leroi, dave, boyd,
|
||||
carter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,4 +347,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
|
||||
// DATAMONGO-1752
|
||||
Iterable<PersonSummary> findClosedProjectionBy();
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
List<Person> findByAgeGreaterThan(int age);
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
List<Person> findByAgeGreaterThan(int age, Sort sort);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import static org.junit.Assert.*;
|
||||
import static org.springframework.data.domain.Sort.Direction.*;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hamcrest.collection.IsIterableContainingInOrder;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -307,6 +309,23 @@ public class ReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanF
|
||||
StepVerifier.create(repository.findFirstByLastname(dave.getLastname())).expectNextCount(1).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void findAppliesAnnotatedSort() {
|
||||
|
||||
repository.findByAgeGreaterThan(40).collectList().as(StepVerifier::create).consumeNextWith(result -> {
|
||||
assertThat(result, IsIterableContainingInOrder.contains(carter, boyd, dave, leroi));
|
||||
});
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void findWithSortOverwritesAnnotatedSort() {
|
||||
|
||||
repository.findByAgeGreaterThan(40, Sort.by(Direction.ASC, "age")).collectList().as(StepVerifier::create)
|
||||
.consumeNextWith(result -> {
|
||||
assertThat(result, IsIterableContainingInOrder.contains(leroi, dave, boyd, carter));
|
||||
});
|
||||
}
|
||||
|
||||
interface ReactivePersonRepository extends ReactiveMongoRepository<Person, String> {
|
||||
|
||||
Flux<Person> findByLastname(String lastname);
|
||||
@@ -335,6 +354,12 @@ public class ReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanF
|
||||
Flux<Person> findPersonByLocationNear(Point point, Distance maxDistance);
|
||||
|
||||
Mono<Person> findFirstByLastname(String lastname);
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
Flux<Person> findByAgeGreaterThan(int age);
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
Flux<Person> findByAgeGreaterThan(int age, Sort sort);
|
||||
}
|
||||
|
||||
interface ReactiveCappedCollectionRepository extends Repository<Capped, String> {
|
||||
|
||||
@@ -17,8 +17,9 @@ package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -39,6 +40,7 @@ import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.mongodb.MongoDbFactory;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
|
||||
@@ -284,6 +286,28 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind).as(DynamicallyMapped.class);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void usesAnnotatedSortWhenPresent() {
|
||||
|
||||
createQueryForMethod("findByAge", Integer.class) //
|
||||
.execute(new Object[] { 1000 });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", 1))));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
public void usesExplicitSortOverridesAnnotatedSortWhenPresent() {
|
||||
|
||||
createQueryForMethod("findByAge", Integer.class, Sort.class) //
|
||||
.execute(new Object[] { 1000, Sort.by(Direction.DESC, "age") });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", -1))));
|
||||
}
|
||||
|
||||
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
return createQueryForMethod(Repo.class, methodName, paramTypes);
|
||||
}
|
||||
@@ -370,6 +394,12 @@ public class AbstractMongoQueryUnitTests {
|
||||
Optional<Person> findByLastname(String lastname);
|
||||
|
||||
Person findFirstByLastname(String lastname);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
|
||||
List<Person> findByAge(Integer age);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
|
||||
List<Person> findByAge(Integer age, Sort page);
|
||||
}
|
||||
|
||||
// DATAMONGO-1872
|
||||
|
||||
@@ -395,6 +395,36 @@ public interface PersonRepository extends MongoRepository<Person, String>
|
||||
|
||||
The query in the preceding example returns only the `firstname`, `lastname` and `Id` properties of the `Person` objects. The `age` property, a `java.lang.Integer`, is not set and its value is therefore null.
|
||||
|
||||
[[mongodb.repositories.queries.sort]]
|
||||
=== Sorting Query Method results
|
||||
|
||||
When it comes to sorting MongoDB query results via the repository interface there are several options as listed below.
|
||||
|
||||
.Sorting query results
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
public interface PersonRepository extends MongoRepository<Person, String> {
|
||||
|
||||
List<Person> findByFirstnameSortByAgeDesc(String firstname); <1>
|
||||
|
||||
List<Person> findByFirstname(String firstname, Sort sort); <2>
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
List<Person> findByFirstname(String firstname); <3>
|
||||
|
||||
@Query(sort = "{ age : -1 }")
|
||||
List<Person> findByLastname(String lastname, Sort sort); <4>
|
||||
}
|
||||
----
|
||||
<1> Fixed sorting derived from method name. `SortByAgeDesc` results in `{ age : -1 }` sort parameter.
|
||||
<2> Dynamic sorting via method argument. `Sort.by(DESC, "age")` creates a `{ age : -1 }` sort parameter.
|
||||
<3> Fixed sorting via `Query` annotation. Sort parameter applied as stated in the `sort` attribute.
|
||||
<4> Default sorting via `Query` annotation combined with dynamic one via method argument. `Sort.unsorted()`
|
||||
results in `{ age : -1 }`. Using `Sort.by(ASC, "age")` overrides the defaults and creates `{ age : 1 }`. `Sort.by
|
||||
(ASC, "firstname")` alters the default and results in `{ age : -1, firstname : 1 }`.
|
||||
====
|
||||
|
||||
[[mongodb.repositories.queries.json-spel]]
|
||||
=== JSON-based Queries with SpEL Expressions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user