diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
index c1d1424ce..3e04d56ba 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
@@ -77,4 +77,22 @@ public @interface Query {
* @return
*/
boolean delete() default false;
+
+ /**
+ * Defines a default sort order for the given query.
+ * NOTE The so set defaults can be altered / overwritten via an explicit
+ * {@link org.springframework.data.domain.Sort} argument of the query method.
+ *
+ *
+ *
+ *
+ * @Query(sort = "{ age : -1 }") // order by age descending
+ * List findByFirstname(String firstname);
+ *
+ *
+ *
+ * @return
+ * @since 2.1
+ */
+ String sort() default "";
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
index 27c706688..bbb77a9b9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
@@ -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
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
index 2cd617dad..53427726a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
@@ -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
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
index e0a0184f5..a2fcf9e29 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
@@ -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, Optional> 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 Optional doFindAnnotation(Class annotationType) {
+
+ return (Optional) this.annotationCache.computeIfAbsent(annotationType, (it) -> {
+ return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it));
+ });
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
new file mode 100644
index 000000000..68d953ca7
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java
@@ -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();
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
index e650c3837..9f19b8af2 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
@@ -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);
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
index 44ca0f0b5..7745b524d 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
@@ -347,4 +347,10 @@ public interface PersonRepository extends MongoRepository, Query
// DATAMONGO-1752
Iterable findClosedProjectionBy();
+
+ @Query(sort = "{ age : -1 }")
+ List findByAgeGreaterThan(int age);
+
+ @Query(sort = "{ age : -1 }")
+ List findByAgeGreaterThan(int age, Sort sort);
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
index 3f6948dc0..ade137d1b 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java
@@ -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 {
Flux findByLastname(String lastname);
@@ -335,6 +354,12 @@ public class ReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanF
Flux findPersonByLocationNear(Point point, Distance maxDistance);
Mono findFirstByLastname(String lastname);
+
+ @Query(sort = "{ age : -1 }")
+ Flux findByAgeGreaterThan(int age);
+
+ @Query(sort = "{ age : -1 }")
+ Flux findByAgeGreaterThan(int age, Sort sort);
}
interface ReactiveCappedCollectionRepository extends Repository {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
index 6f28dee9f..7ec17a4c5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
+++ b/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;
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 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 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 findByLastname(String lastname);
Person findFirstByLastname(String lastname);
+
+ @org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
+ List findByAge(Integer age);
+
+ @org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
+ List findByAge(Integer age, Sort page);
}
// DATAMONGO-1872
diff --git a/src/main/asciidoc/reference/mongo-repositories.adoc b/src/main/asciidoc/reference/mongo-repositories.adoc
index 83cb05884..93c00db0c 100644
--- a/src/main/asciidoc/reference/mongo-repositories.adoc
+++ b/src/main/asciidoc/reference/mongo-repositories.adoc
@@ -395,6 +395,36 @@ public interface PersonRepository extends MongoRepository
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 {
+
+ List findByFirstnameSortByAgeDesc(String firstname); <1>
+
+ List findByFirstname(String firstname, Sort sort); <2>
+
+ @Query(sort = "{ age : -1 }")
+ List findByFirstname(String firstname); <3>
+
+ @Query(sort = "{ age : -1 }")
+ List 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