From c5129aca4552de97b20cd23a7a5624b50110b528 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 4 Jun 2018 15:18:34 +0200 Subject: [PATCH] =?UTF-8?q?DATAMONGO-1979=20-=20Add=20default=20sorting=20?= =?UTF-8?q?for=20repository=20query=20methods=20using=20@Query(sort=20=3D?= =?UTF-8?q?=20"=E2=80=A6").?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now allow to set a default sort for repository query methods via the @Query annotation. @Query(sort = "{ age : -1 }") List 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 findByFirstname(String firstname, Sort sort); Original pull request: #566. --- .../data/mongodb/repository/Query.java | 18 ++++++ .../repository/query/AbstractMongoQuery.java | 21 ++++++- .../query/AbstractReactiveMongoQuery.java | 19 ++++++ .../repository/query/MongoQueryMethod.java | 36 +++++++++++ .../mongodb/repository/query/QueryUtils.java | 61 +++++++++++++++++++ ...tractPersonRepositoryIntegrationTests.java | 11 ++++ .../mongodb/repository/PersonRepository.java | 6 ++ .../ReactiveMongoRepositoryTests.java | 25 ++++++++ .../query/AbstractMongoQueryUnitTests.java | 32 +++++++++- .../reference/mongo-repositories.adoc | 30 +++++++++ 10 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java 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, 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