diff --git a/pom.xml b/pom.xml index 7227da3581..3cfbfad13e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..0271160e84 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 47a5b7aba7..503a52449e 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT ../pom.xml @@ -50,7 +50,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..69c7bcd3e7 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b86dc2808c..364f47a933 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1979-SNAPSHOT ../pom.xml 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 c1d1424ce9..3e04d56ba5 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 @@ * @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 27c7066887..bbb77a9b94 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 Object execute(Object[] parameters) { Query query = createQuery(accessor); applyQueryMetaAttributesWhenPresent(query); + query = applyAnnotatedDefaultSortIfPresent(query); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); Class typeToRead = processor.getReturnedType().getTypeToRead(); @@ -110,7 +112,7 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F } 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 @@ Query applyQueryMetaAttributesWhenPresent(Query query) { 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 2cd617dadb..53427726a1 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 @@ private Object execute(MongoParameterAccessor parameterAccessor) { 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 @@ Query applyQueryMetaAttributesWhenPresent(Query query) { 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 e0a0184f5f..2aa8e8edf1 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,13 +16,14 @@ 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; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; @@ -40,6 +41,7 @@ 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 +59,7 @@ public class MongoQueryMethod extends QueryMethod { private final Method method; private final MappingContext, MongoPersistentProperty> mappingContext; + private final Map, Optional> annotationCache; private @Nullable MongoEntityMetadata metadata; @@ -77,6 +80,7 @@ public MongoQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa this.method = method; this.mappingContext = mappingContext; + this.annotationCache = new ConcurrentReferenceHashMap(); } /* @@ -110,9 +114,8 @@ String getAnnotatedQuery() { private Optional findAnnotatedQuery() { - return Optional.ofNullable(getQueryAnnotation()) // - .map(AnnotationUtils::getValue) // - .map(it -> (String) it) // + return lookupQueryAnnotation() // + .map(Query::value) // .filter(StringUtils::hasText); } @@ -123,8 +126,8 @@ private Optional findAnnotatedQuery() { */ String getFieldSpecification() { - return Optional.ofNullable(getQueryAnnotation()) // - .map(it -> (String) AnnotationUtils.getValue(it, "fields")) // + return lookupQueryAnnotation() // + .map(Query::fields) // .filter(StringUtils::hasText) // .orElse(null); } @@ -207,7 +210,11 @@ private boolean isGeoNearQuery(Method method) { */ @Nullable Query getQueryAnnotation() { - return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + return lookupQueryAnnotation().orElse(null); + } + + Optional lookupQueryAnnotation() { + return doFindAnnotation(Query.class); } TypeInformation getReturnType() { @@ -230,7 +237,7 @@ public boolean hasQueryMetaAttributes() { */ @Nullable Meta getMetaAnnotation() { - return AnnotatedElementUtils.findMergedAnnotation(method, Meta.class); + return doFindAnnotation(Meta.class).orElse(null); } /** @@ -241,7 +248,7 @@ Meta getMetaAnnotation() { */ @Nullable Tailable getTailableAnnotation() { - return AnnotatedElementUtils.findMergedAnnotation(method, Tailable.class); + return doFindAnnotation(Tailable.class).orElse(null); } /** @@ -283,4 +290,34 @@ public org.springframework.data.mongodb.core.query.Meta getQueryMetaAttributes() 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 lookupQueryAnnotation().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 lookupQueryAnnotation().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 -> 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 0000000000..68d953ca77 --- /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 e650c38378..9f19b8af27 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 void findSingleEntityThrowsErrorWhenNotUnique() { 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 44ca0f0b5c..7745b524df 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 @@ Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li // 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 3f6948dc06..ade137d1b1 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.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 void shouldReturnFirstFindFirstWithMoreResults() { 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 @@ interface ReactivePersonRepository extends ReactiveMongoRepository 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 6f28dee9f9..7ec17a4c58 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 @@ 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.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 void doesNotFixCollectionOnPreparation() { 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 @@ private interface Repo extends MongoRepository { 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 83cb05884e..93c00db0c2 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