Skip to content

Commit da656e0

Browse files
Fix conversion of types when mapping Aggregation pipeline.
This change makes sure to apply conversion to non native mongo types when the context does not expose fields.
1 parent a922212 commit da656e0

File tree

2 files changed

+89
-21
lines changed

2 files changed

+89
-21
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ static List<Document> toDocument(List<AggregationOperation> operations, Aggregat
6262
if (operation instanceof InheritsFieldsAggregationOperation || exposedFieldsOperation.inheritsFields()) {
6363
contextToUse = contextToUse.inheritAndExpose(fields);
6464
} else {
65-
contextToUse = fields.exposesNoFields() ? DEFAULT_CONTEXT
65+
contextToUse = fields.exposesNoFields() ? ConverterAwareNoOpContext.instance(rootContext)
6666
: contextToUse.expose(fields);
6767
}
6868
}
@@ -72,6 +72,39 @@ static List<Document> toDocument(List<AggregationOperation> operations, Aggregat
7272
return operationDocuments;
7373
}
7474

75+
private static class ConverterAwareNoOpContext implements AggregationOperationContext {
76+
77+
AggregationOperationContext ctx;
78+
79+
static ConverterAwareNoOpContext instance(AggregationOperationContext ctx) {
80+
81+
if(ctx instanceof ConverterAwareNoOpContext noOpContext) {
82+
return noOpContext;
83+
}
84+
85+
return new ConverterAwareNoOpContext(ctx);
86+
}
87+
88+
ConverterAwareNoOpContext(AggregationOperationContext ctx) {
89+
this.ctx = ctx;
90+
}
91+
92+
@Override
93+
public Document getMappedObject(Document document, @Nullable Class<?> type) {
94+
return ctx.getMappedObject(document, null);
95+
}
96+
97+
@Override
98+
public FieldReference getReference(Field field) {
99+
return new DirectFieldReference(new ExposedField(field, true));
100+
}
101+
102+
@Override
103+
public FieldReference getReference(String name) {
104+
return new DirectFieldReference(new ExposedField(new AggregationField(name), true));
105+
}
106+
}
107+
75108
/**
76109
* Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is.
77110
*

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRendererUnitTests.java

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,29 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18-
import static org.mockito.Mockito.*;
19-
import static org.springframework.data.domain.Sort.Direction.*;
20-
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
21-
18+
import static org.mockito.Mockito.eq;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.verify;
21+
import static org.springframework.data.domain.Sort.Direction.DESC;
22+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
23+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.sort;
24+
25+
import java.time.ZonedDateTime;
2226
import java.util.List;
27+
import java.util.Set;
2328

29+
import org.assertj.core.api.Assertions;
30+
import org.bson.Document;
2431
import org.junit.jupiter.api.Test;
25-
2632
import org.springframework.data.annotation.Id;
33+
import org.springframework.data.convert.ConverterBuilder;
34+
import org.springframework.data.convert.CustomConversions;
35+
import org.springframework.data.convert.CustomConversions.StoreConversions;
36+
import org.springframework.data.domain.Sort.Direction;
2737
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
2838
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
2939
import org.springframework.data.mongodb.core.convert.QueryMapper;
40+
import org.springframework.data.mongodb.core.query.Criteria;
3041
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
3142

3243
/**
@@ -47,32 +58,56 @@ void nonFieldsExposingAggregationOperationContinuesWithSameContextForNextStage()
4758
verify(stage2).toPipelineStages(eq(rootContext));
4859
}
4960

50-
record TestRecord(@Id String field1, String field2, LayerOne layerOne) {
51-
record LayerOne(List<LayerTwo> layerTwo) {
52-
}
61+
@Test
62+
void contextShouldCarryOnRelaxedFieldMapping() {
5363

54-
record LayerTwo(LayerThree layerThree) {
55-
}
64+
MongoTestMappingContext ctx = new MongoTestMappingContext(cfg -> {
65+
cfg.initialEntitySet(TestRecord.class);
66+
});
67+
68+
MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, ctx);
5669

57-
record LayerThree(int fieldA, int fieldB)
58-
{}
70+
Aggregation agg = Aggregation.newAggregation(Aggregation.unwind("layerOne.layerTwo"),
71+
project().and("layerOne.layerTwo.layerThree").as("layerOne.layerThree"),
72+
sort(DESC, "layerOne.layerThree.fieldA"));
73+
74+
AggregationOperationRenderer.toDocument(agg.getPipeline().getOperations(),
75+
new RelaxedTypeBasedAggregationOperationContext(TestRecord.class, ctx, new QueryMapper(mongoConverter)));
5976
}
6077

61-
@Test
62-
void xxx() {
78+
@Test // GH-4722
79+
void appliesConversionToValuesUsedInAggregation() {
6380

6481
MongoTestMappingContext ctx = new MongoTestMappingContext(cfg -> {
6582
cfg.initialEntitySet(TestRecord.class);
6683
});
6784

6885
MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, ctx);
69-
70-
Aggregation agg = Aggregation.newAggregation(
71-
Aggregation.unwind("layerOne.layerTwo"),
72-
project().and("layerOne.layerTwo.layerThree").as("layerOne.layerThree"),
73-
sort(DESC, "layerOne.layerThree.fieldA")
86+
mongoConverter.setCustomConversions(new CustomConversions(StoreConversions.NONE,
87+
Set.copyOf(ConverterBuilder.writing(ZonedDateTime.class, String.class, ZonedDateTime::toString)
88+
.andReading(it -> ZonedDateTime.parse(it)).getConverters())));
89+
mongoConverter.afterPropertiesSet();
90+
91+
var agg = Aggregation.newAggregation(Aggregation.sort(Direction.DESC, "version"),
92+
Aggregation.group("entityId").first(Aggregation.ROOT).as("value"), Aggregation.replaceRoot("value"),
93+
Aggregation.match(Criteria.where("createdDate").lt(ZonedDateTime.now())) // here is the problem
7494
);
7595

76-
AggregationOperationRenderer.toDocument(agg.getPipeline().getOperations(), new RelaxedTypeBasedAggregationOperationContext(TestRecord.class, ctx, new QueryMapper(mongoConverter)));
96+
List<Document> document = AggregationOperationRenderer.toDocument(agg.getPipeline().getOperations(),
97+
new RelaxedTypeBasedAggregationOperationContext(TestRecord.class, ctx, new QueryMapper(mongoConverter)));
98+
Assertions.assertThat(document).last()
99+
.extracting(it -> it.getEmbedded(List.of("$match", "createdDate", "$lt"), Object.class))
100+
.isInstanceOf(String.class);
101+
}
102+
103+
record TestRecord(@Id String field1, String field2, LayerOne layerOne) {
104+
record LayerOne(List<LayerTwo> layerTwo) {
105+
}
106+
107+
record LayerTwo(LayerThree layerThree) {
108+
}
109+
110+
record LayerThree(int fieldA, int fieldB) {
111+
}
77112
}
78113
}

0 commit comments

Comments
 (0)