Browse Source

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.
pull/568/merge
Christoph Strobl 8 years ago committed by Mark Paluch
parent
commit
c5129aca45
  1. 18
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
  2. 21
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
  3. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
  4. 36
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
  5. 61
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
  6. 11
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  7. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  8. 25
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
  9. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
  10. 30
      src/main/asciidoc/reference/mongo-repositories.adoc

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

@ -77,4 +77,22 @@ public @interface Query { @@ -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>
*
* &#64;Query(sort = "{ age : -1 }") // order by age descending
* List<Person> findByFirstname(String firstname);
* </code>
* </pre>
*
* @return
* @since 2.1
*/
String sort() default "";
}

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

@ -15,6 +15,7 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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

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

@ -15,6 +15,7 @@ @@ -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 { @@ -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 { @@ -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

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

@ -16,9 +16,11 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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));
});
}
}

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

@ -0,0 +1,61 @@ @@ -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();
}
}

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

@ -1205,4 +1205,15 @@ public abstract class AbstractPersonRepositoryIntegrationTests { @@ -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);
}
}

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

@ -347,4 +347,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -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);
}

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

@ -20,6 +20,8 @@ import static org.junit.Assert.*; @@ -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 @@ -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 @@ -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> {

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

@ -17,8 +17,9 @@ package org.springframework.data.mongodb.repository.query; @@ -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; @@ -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 { @@ -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 { @@ -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

30
src/main/asciidoc/reference/mongo-repositories.adoc

@ -395,6 +395,36 @@ public interface PersonRepository extends MongoRepository<Person, String> @@ -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

Loading…
Cancel
Save