Skip to content

Commit 6304af3

Browse files
christophstroblmp911de
authored andcommitted
Fix interface projections for string based aggregations.
Closes #4839 Original pull request: #4841
1 parent e882594 commit 6304af3

File tree

4 files changed

+44
-24
lines changed

4 files changed

+44
-24
lines changed

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper
106106
@Override
107107
@Nullable
108108
protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor,
109-
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
109+
ConvertingParameterAccessor accessor, @Nullable Class<?> typeToRead) {
110110

111111
Class<?> sourceType = method.getDomainClass();
112112
Class<?> targetType = typeToRead;
@@ -121,15 +121,17 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProces
121121
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
122122
}
123123

124-
boolean isSimpleReturnType = isSimpleReturnType(typeToRead);
125-
boolean isRawAggregationResult = ClassUtils.isAssignable(AggregationResults.class, typeToRead);
124+
boolean isSimpleReturnType = typeToRead != null && isSimpleReturnType(typeToRead);
125+
boolean isRawAggregationResult = typeToRead != null && ClassUtils.isAssignable(AggregationResults.class, typeToRead);
126126

127127
if (isSimpleReturnType) {
128128
targetType = Document.class;
129129
} else if (isRawAggregationResult) {
130130

131131
// 🙈
132132
targetType = method.getReturnType().getRequiredActualType().getRequiredComponentType().getType();
133+
} else if (resultProcessor.getReturnedType().isProjecting()) {
134+
targetType = resultProcessor.getReturnedType().getReturnedType().isInterface() ? Document.class :resultProcessor.getReturnedType().getReturnedType();
133135
}
134136

135137
AggregationOptions options = computeOptions(method, accessor, pipeline);
@@ -147,7 +149,7 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProces
147149
}
148150

149151
AggregationResults<Object> result = (AggregationResults<Object>) mongoOperations.aggregate(aggregation, targetType);
150-
if (ReflectionUtils.isVoid(typeToRead)) {
152+
if (typeToRead != null && ReflectionUtils.isVoid(typeToRead)) {
151153
return null;
152154
}
153155

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

+9-5
Original file line numberDiff line numberDiff line change
@@ -1275,11 +1275,6 @@ void readsClosedProjection() {
12751275
assertThat(repository.findClosedProjectionBy()).isNotEmpty();
12761276
}
12771277

1278-
@Test // https://github.com/spring-projects/spring-data-mongodb/issues/4839
1279-
void findAggregatedClosedProjectionBy() {
1280-
assertThat(repository.findAggregatedClosedProjectionBy()).isNotEmpty();
1281-
}
1282-
12831278
@Test // DATAMONGO-1865
12841279
void findFirstEntityReturnsFirstResultEvenForNonUniqueMatches() {
12851280
assertThat(repository.findFirstBy()).isNotNull();
@@ -1464,6 +1459,15 @@ void annotatedAggregationWithAggregationResultAsReturnTypeAndProjection() {
14641459
.containsExactly(new SumAge(245L));
14651460
}
14661461

1462+
@Test // GH-4839
1463+
void annotatedAggregationWithAggregationResultAsClosedInterfaceProjection() {
1464+
1465+
assertThat(repository.findAggregatedClosedInterfaceProjectionBy()).allSatisfy(it -> {
1466+
assertThat(it.getFirstname()).isIn(dave.getFirstname(), oliver.getFirstname());
1467+
assertThat(it.getLastname()).isEqualTo(dave.getLastname());
1468+
});
1469+
}
1470+
14671471
@Test // DATAMONGO-2374
14681472
void findsWithNativeProjection() {
14691473

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,6 @@ Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li
386386
// DATAMONGO-1752
387387
Iterable<PersonSummary> findClosedProjectionBy();
388388

389-
// https://github.com/spring-projects/spring-data-mongodb/issues/4839
390-
@Aggregation("{ '$project': { _id : 0, firstName : 1, lastname : 1 } }")
391-
Iterable<PersonSummary> findAggregatedClosedProjectionBy();
392-
393389
@Query(sort = "{ age : -1 }")
394390
List<Person> findByAgeGreaterThan(int age);
395391

@@ -438,6 +434,12 @@ Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li
438434
@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
439435
AggregationResults<SumAge> sumAgeAndReturnAggregationResultWrapperWithConcreteType();
440436

437+
@Aggregation({
438+
"{ '$match' : { 'lastname' : 'Matthews'} }",
439+
"{ '$project': { _id : 0, firstname : 1, lastname : 1 } }"
440+
})
441+
Iterable<PersonSummary> findAggregatedClosedInterfaceProjectionBy();
442+
441443
@Query(value = "{_id:?0}")
442444
Optional<org.bson.Document> findDocumentById(String id);
443445

src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc

+23-11
Original file line numberDiff line numberDiff line change
@@ -571,23 +571,29 @@ public interface PersonRepository extends CrudRepository<Person, String> {
571571
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
572572
Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream(); <5>
573573
574+
@Aggregation(pipeline = {
575+
"{ '$match' : { 'lastname' : '?0'} }",
576+
"{ '$project': { _id : 0, firstname : 1, lastname : 1 } }"
577+
})
578+
Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream(); <6>
579+
574580
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
575-
SumValue sumAgeUsingValueWrapper(); <6>
581+
SumValue sumAgeUsingValueWrapper(); <7>
576582
577583
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
578-
Long sumAge(); <7>
584+
Long sumAge(); <8>
579585
580586
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
581-
AggregationResults<SumValue> sumAgeRaw(); <8>
587+
AggregationResults<SumValue> sumAgeRaw(); <9>
582588
583589
@Aggregation("{ '$project': { '_id' : '$lastname' } }")
584-
List<String> findAllLastnames(); <9>
590+
List<String> findAllLastnames(); <10>
585591
586592
@Aggregation(pipeline = {
587593
"{ $group : { _id : '$author', books: { $push: '$title' } } }",
588594
"{ $out : 'authors' }"
589595
})
590-
void groupAndOutSkippingOutput(); <10>
596+
void groupAndOutSkippingOutput(); <11>
591597
}
592598
----
593599
[source,java]
@@ -614,19 +620,25 @@ public class SumValue {
614620
615621
// Getter omitted
616622
}
623+
624+
interface PersonProjection {
625+
String getFirstname();
626+
String getLastname();
627+
}
617628
----
618629
<1> Aggregation pipeline to group first names by `lastname` in the `Person` collection returning these as `PersonAggregate`.
619630
<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.
620631
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`.
621632
<3> Replaces `?0` with the given value for `property` for a dynamic aggregation pipeline.
622633
<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.
623-
<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`.
624-
<6> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type.
625-
<7> Aggregations resulting in single document holding just an accumulation result like e.g. `$sum` can be extracted directly from the result `Document`.
634+
<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.
635+
<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`.
636+
<7> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type.
637+
<8> Aggregations resulting in single document holding just an accumulation result like e.g. `$sum` can be extracted directly from the result `Document`.
626638
To gain more control, you might consider `AggregationResult` as method return type as shown in <7>.
627-
<8> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
628-
<9> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.
629-
<10> Skips the output of the `$out` stage when return type is `void`.
639+
<9> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
640+
<10> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.
641+
<11> Skips the output of the `$out` stage when return type is `void`.
630642
====
631643

632644
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.

0 commit comments

Comments
 (0)