diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 1ffca4d85..c2ba74328 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -106,7 +106,7 @@ public class StringBasedAggregation extends AbstractMongoQuery { @Override @Nullable protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor, - ConvertingParameterAccessor accessor, Class typeToRead) { + ConvertingParameterAccessor accessor, @Nullable Class typeToRead) { Class sourceType = method.getDomainClass(); Class targetType = typeToRead; @@ -121,8 +121,8 @@ public class StringBasedAggregation extends AbstractMongoQuery { AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor); } - boolean isSimpleReturnType = isSimpleReturnType(typeToRead); - boolean isRawAggregationResult = ClassUtils.isAssignable(AggregationResults.class, typeToRead); + boolean isSimpleReturnType = typeToRead != null && isSimpleReturnType(typeToRead); + boolean isRawAggregationResult = typeToRead != null && ClassUtils.isAssignable(AggregationResults.class, typeToRead); if (isSimpleReturnType) { targetType = Document.class; @@ -130,6 +130,8 @@ public class StringBasedAggregation extends AbstractMongoQuery { // 🙈 targetType = method.getReturnType().getRequiredActualType().getRequiredComponentType().getType(); + } else if (resultProcessor.getReturnedType().isProjecting()) { + targetType = resultProcessor.getReturnedType().getReturnedType().isInterface() ? Document.class :resultProcessor.getReturnedType().getReturnedType(); } AggregationOptions options = computeOptions(method, accessor, pipeline); @@ -147,7 +149,7 @@ public class StringBasedAggregation extends AbstractMongoQuery { } AggregationResults result = (AggregationResults) mongoOperations.aggregate(aggregation, targetType); - if (ReflectionUtils.isVoid(typeToRead)) { + if (typeToRead != null && ReflectionUtils.isVoid(typeToRead)) { return null; } 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 20e333e34..12f782195 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 @@ -1275,11 +1275,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(repository.findClosedProjectionBy()).isNotEmpty(); } - @Test // https://github.com/spring-projects/spring-data-mongodb/issues/4839 - void findAggregatedClosedProjectionBy() { - assertThat(repository.findAggregatedClosedProjectionBy()).isNotEmpty(); - } - @Test // DATAMONGO-1865 void findFirstEntityReturnsFirstResultEvenForNonUniqueMatches() { assertThat(repository.findFirstBy()).isNotNull(); @@ -1464,6 +1459,15 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie .containsExactly(new SumAge(245L)); } + @Test // GH-4839 + void annotatedAggregationWithAggregationResultAsClosedInterfaceProjection() { + + assertThat(repository.findAggregatedClosedInterfaceProjectionBy()).allSatisfy(it -> { + assertThat(it.getFirstname()).isIn(dave.getFirstname(), oliver.getFirstname()); + assertThat(it.getLastname()).isEqualTo(dave.getLastname()); + }); + } + @Test // DATAMONGO-2374 void findsWithNativeProjection() { 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 09ead8061..de8b1f569 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 @@ -386,10 +386,6 @@ public interface PersonRepository extends MongoRepository, Query // DATAMONGO-1752 Iterable findClosedProjectionBy(); - // https://github.com/spring-projects/spring-data-mongodb/issues/4839 - @Aggregation("{ '$project': { _id : 0, firstName : 1, lastname : 1 } }") - Iterable findAggregatedClosedProjectionBy(); - @Query(sort = "{ age : -1 }") List findByAgeGreaterThan(int age); @@ -438,6 +434,12 @@ public interface PersonRepository extends MongoRepository, Query @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") AggregationResults sumAgeAndReturnAggregationResultWrapperWithConcreteType(); + @Aggregation({ + "{ '$match' : { 'lastname' : 'Matthews'} }", + "{ '$project': { _id : 0, firstname : 1, lastname : 1 } }" + }) + Iterable findAggregatedClosedInterfaceProjectionBy(); + @Query(value = "{_id:?0}") Optional findDocumentById(String id); diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc index fa9331136..adb2392f0 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc @@ -571,23 +571,29 @@ public interface PersonRepository extends CrudRepository { @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }") Stream groupByLastnameAndFirstnamesAsStream(); <5> + @Aggregation(pipeline = { + "{ '$match' : { 'lastname' : '?0'} }", + "{ '$project': { _id : 0, firstname : 1, lastname : 1 } }" + }) + Stream groupByLastnameAndFirstnamesAsStream(); <6> + @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") - SumValue sumAgeUsingValueWrapper(); <6> + SumValue sumAgeUsingValueWrapper(); <7> @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") - Long sumAge(); <7> + Long sumAge(); <8> @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") - AggregationResults sumAgeRaw(); <8> + AggregationResults sumAgeRaw(); <9> @Aggregation("{ '$project': { '_id' : '$lastname' } }") - List findAllLastnames(); <9> + List findAllLastnames(); <10> @Aggregation(pipeline = { "{ $group : { _id : '$author', books: { $push: '$title' } } }", "{ $out : 'authors' }" }) - void groupAndOutSkippingOutput(); <10> + void groupAndOutSkippingOutput(); <11> } ---- [source,java] @@ -614,19 +620,25 @@ public class SumValue { // Getter omitted } + +interface PersonProjection { + String getFirstname(); + String getLastname(); +} ---- <1> Aggregation pipeline to group first names by `lastname` in the `Person` collection returning these as `PersonAggregate`. <2> If `Sort` argument is present, `$sort` is appended after the declared pipeline stages so that it only affects the order of the final results after having passed all other aggregation stages. Therefore, the `Sort` properties are mapped against the methods return type `PersonAggregate` which turns `Sort.by("lastname")` into `{ $sort : { '_id', 1 } }` because `PersonAggregate.lastname` is annotated with `@Id`. <3> Replaces `?0` with the given value for `property` for a dynamic aggregation pipeline. <4> `$skip`, `$limit` and `$sort` can be passed on via a `Pageable` argument. Same as in <2>, the operators are appended to the pipeline definition. Methods accepting `Pageable` can return `Slice` for easier pagination. -<5> Aggregation methods can return `Stream` to consume results directly from an underlying cursor. Make sure to close the stream after consuming it to release the server-side cursor by either calling `close()` or through `try-with-resources`. -<6> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type. -<7> Aggregations resulting in single document holding just an accumulation result like e.g. `$sum` can be extracted directly from the result `Document`. +<5> Aggregation methods can return interface based projections wrapping the resulting `org.bson.Document` behind a proxy, exposing getters delegating to fields within the document. +<6> Aggregation methods can return `Stream` to consume results directly from an underlying cursor. Make sure to close the stream after consuming it to release the server-side cursor by either calling `close()` or through `try-with-resources`. +<7> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type. +<8> Aggregations resulting in single document holding just an accumulation result like e.g. `$sum` can be extracted directly from the result `Document`. To gain more control, you might consider `AggregationResult` as method return type as shown in <7>. -<8> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`. -<9> Like in <6>, a single value can be directly obtained from multiple result ``Document``s. -<10> Skips the output of the `$out` stage when return type is `void`. +<9> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`. +<10> Like in <6>, a single value can be directly obtained from multiple result ``Document``s. +<11> Skips the output of the `$out` stage when return type is `void`. ==== In some scenarios, aggregations might require additional options, such as a maximum run time, additional log comments, or the permission to temporarily write data to disk.