Skip to content

Commit 9983390

Browse files
Include all fields on sort/project of embedded object
move
1 parent e27fecc commit 9983390

File tree

9 files changed

+246
-21
lines changed

9 files changed

+246
-21
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.mapping.PersistentEntity;
3333
import org.springframework.data.mapping.PersistentProperty;
3434
import org.springframework.data.mapping.PersistentPropertyPath;
35+
import org.springframework.data.mapping.PropertyHandler;
3536
import org.springframework.data.mapping.PropertyPath;
3637
import org.springframework.data.mapping.PropertyReferenceException;
3738
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
@@ -187,11 +188,13 @@ public Document getMappedSort(Document sortObject, @Nullable MongoPersistentEnti
187188
return new Document();
188189
}
189190

191+
sortObject = filterEmbeddedObjects(sortObject, entity);
192+
190193
Document mappedSort = new Document();
191194
for (Map.Entry<String, Object> entry : BsonUtils.asMap(sortObject).entrySet()) {
192195

193196
Field field = createPropertyField(entity, entry.getKey(), mappingContext);
194-
if(field.getProperty() != null && field.getProperty().isEmbedded()) {
197+
if (field.getProperty() != null && field.getProperty().isEmbedded()) {
195198
continue;
196199
}
197200

@@ -215,7 +218,9 @@ public Document getMappedFields(Document fieldsObject, @Nullable MongoPersistent
215218

216219
Assert.notNull(fieldsObject, "FieldsObject must not be null!");
217220

218-
Document mappedFields = fieldsObject.isEmpty() ? new Document() : getMappedObject(fieldsObject, entity);
221+
fieldsObject = filterEmbeddedObjects(fieldsObject, entity);
222+
223+
Document mappedFields = getMappedObject(fieldsObject, entity);
219224
mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE);
220225
return mappedFields;
221226
}
@@ -235,6 +240,43 @@ private void mapMetaAttributes(Document source, @Nullable MongoPersistentEntity<
235240
}
236241
}
237242

243+
private Document filterEmbeddedObjects(Document fieldsObject, @Nullable MongoPersistentEntity<?> entity) {
244+
245+
if (fieldsObject.isEmpty() || entity == null) {
246+
return fieldsObject;
247+
}
248+
249+
Document target = new Document();
250+
251+
for (Entry<String, Object> field : fieldsObject.entrySet()) {
252+
253+
try {
254+
255+
PropertyPath path = PropertyPath.from(field.getKey(), entity.getTypeInformation());
256+
PersistentPropertyPath<MongoPersistentProperty> persistentPropertyPath = mappingContext
257+
.getPersistentPropertyPath(path);
258+
MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getLeafProperty();
259+
260+
if (property.isEmbedded() && property.isEntity()) {
261+
262+
mappingContext.getPersistentEntity(property)
263+
.doWithProperties((PropertyHandler<MongoPersistentProperty>) embedded -> {
264+
265+
String dotPath = persistentPropertyPath.toDotPath();
266+
dotPath = dotPath + (StringUtils.hasText(dotPath) ? "." : "") + embedded.getName();
267+
target.put(dotPath, field.getValue());
268+
});
269+
} else {
270+
target.put(field.getKey(), field.getValue());
271+
}
272+
} catch (RuntimeException e) {
273+
target.put(field.getKey(), field.getValue());
274+
}
275+
276+
}
277+
return target;
278+
}
279+
238280
private Document getMappedTextScoreField(MongoPersistentProperty property) {
239281
return new Document(property.getFieldName(), META_TEXT_SCORE);
240282
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, Mongo
136136
try {
137137
if (persistentProperty.isEntity()) {
138138
indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty),
139-
persistentProperty.isEmbedded() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard));
139+
persistentProperty.isEmbedded() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty),
140+
root.getCollection(), guard));
140141
}
141142

142143
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(
@@ -185,7 +186,7 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers
185186

186187
String propertyDotPath = dotPath;
187188

188-
if(!persistentProperty.isEmbedded()) {
189+
if (!persistentProperty.isEmbedded()) {
189190
propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName();
190191
}
191192

@@ -214,6 +215,13 @@ private List<IndexDefinitionHolder> createIndexDefinitionHolderForProperty(Strin
214215

215216
List<IndexDefinitionHolder> indices = new ArrayList<>(2);
216217

218+
if (persistentProperty.isEmbedded() && (persistentProperty.isAnnotationPresent(Indexed.class)
219+
|| persistentProperty.isAnnotationPresent(HashIndexed.class)
220+
|| persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class))) {
221+
throw new InvalidDataAccessApiUsageException(
222+
String.format("Index annotation not allowed on embedded object for path '%s'.", dotPath));
223+
}
224+
217225
if (persistentProperty.isAnnotationPresent(Indexed.class)) {
218226
indices.add(createIndexDefinition(dotPath, collection, persistentProperty));
219227
} else if (persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class)) {
@@ -490,7 +498,7 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
490498
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
491499
}
492500

493-
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?,?> entity) {
501+
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) {
494502

495503
Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity));
496504

@@ -501,7 +509,6 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis
501509
return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null));
502510
}
503511

504-
505512
/**
506513
* Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for a given
507514
* {@link MongoPersistentProperty}.

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ public boolean hasTextScoreProperty() {
164164
@Override
165165
public org.springframework.data.mongodb.core.query.Collation getCollation() {
166166

167-
Object collationValue = collationExpression != null ? collationExpression.getValue(getEvaluationContext(null), String.class)
167+
Object collationValue = collationExpression != null
168+
? collationExpression.getValue(getEvaluationContext(null), String.class)
168169
: this.collation;
169170

170171
if (collationValue == null) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.bson.Document;
2121
import org.junit.jupiter.api.Test;
22+
import org.springframework.data.annotation.Embedded;
2223

2324
/**
2425
* Unit tests for {@link AggregationUpdate}.
@@ -38,4 +39,19 @@ public void createPipelineWithMultipleStages() {
3839
.containsExactly(new Document("$set", new Document("stage-1", "value-1")),
3940
new Document("$unset", "stage-2"), new Document("$set", new Document("stage-3", "value-3")));
4041
}
42+
43+
@Test // DATAMONGO-2331
44+
public void xxx() {
45+
46+
47+
assertThat(AggregationUpdate.update() //
48+
.set("stage-1").toValue("value-1") //
49+
.unset("stage-2") //
50+
.set("stage-3").toValue("value-3") //
51+
.toPipeline(Aggregation.DEFAULT_CONTEXT)) //
52+
.containsExactly(new Document("$set", new Document("stage-1", "value-1")),
53+
new Document("$unset", "stage-2"), new Document("$set", new Document("stage-3", "value-3")));
54+
}
55+
56+
4157
}

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

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
import org.junit.jupiter.api.extension.ExtendWith;
3333
import org.mockito.Mock;
3434
import org.mockito.junit.jupiter.MockitoExtension;
35-
3635
import org.springframework.core.convert.converter.Converter;
3736
import org.springframework.core.convert.support.GenericConversionService;
37+
import org.springframework.data.annotation.Embedded;
3838
import org.springframework.data.annotation.Id;
3939
import org.springframework.data.convert.CustomConversions;
4040
import org.springframework.data.domain.Sort;
@@ -357,6 +357,103 @@ public void projectOperationShouldRenderNestedFieldNamesCorrectlyForTypedAggrega
357357
.isEqualTo(new Document("val", new Document("$add", Arrays.asList("$nested1.value1", "$field2.nestedValue2"))));
358358
}
359359

360+
@Test // DATAMONGO-1902
361+
void rendersProjectOnEmbeddedFieldCorrectly() {
362+
363+
AggregationOperationContext context = getContext(WithEmbedded.class);
364+
365+
Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.stringValue").as("val"))
366+
.toDocument("collection", context);
367+
368+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
369+
.isEqualTo(new Document("val", "$stringValue"));
370+
}
371+
372+
@Test // DATAMONGO-1902
373+
void rendersProjectOnEmbeddedFieldWithAtFieldAnnotationCorrectly() {
374+
375+
AggregationOperationContext context = getContext(WithEmbedded.class);
376+
377+
Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.atFieldAnnotatedValue").as("val"))
378+
.toDocument("collection", context);
379+
380+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
381+
.isEqualTo(new Document("val", "$with-at-field-annotation"));
382+
}
383+
384+
@Test // DATAMONGO-1902
385+
void rendersProjectOnPrefixedEmbeddedFieldCorrectly() {
386+
387+
AggregationOperationContext context = getContext(WithEmbedded.class);
388+
389+
Document agg = newAggregation(WithEmbedded.class, project().and("prefixedEmbeddableValue.stringValue").as("val"))
390+
.toDocument("collection", context);
391+
392+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
393+
.isEqualTo(new Document("val", "$prefix-stringValue"));
394+
}
395+
396+
@Test // DATAMONGO-1902
397+
void rendersProjectOnPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
398+
399+
AggregationOperationContext context = getContext(WithEmbedded.class);
400+
401+
Document agg = newAggregation(WithEmbedded.class,
402+
project().and("prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
403+
404+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
405+
.isEqualTo(new Document("val", "$prefix-with-at-field-annotation"));
406+
}
407+
408+
@Test // DATAMONGO-1902
409+
void rendersProjectOnNestedEmbeddedFieldCorrectly() {
410+
411+
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
412+
413+
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
414+
project().and("withEmbedded.embeddableType.stringValue").as("val")).toDocument("collection", context);
415+
416+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
417+
.isEqualTo(new Document("val", "$withEmbedded.stringValue"));
418+
}
419+
420+
@Test // DATAMONGO-1902
421+
void rendersProjectOnNestedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
422+
423+
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
424+
425+
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
426+
project().and("withEmbedded.embeddableType.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
427+
428+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
429+
.isEqualTo(new Document("val", "$withEmbedded.with-at-field-annotation"));
430+
}
431+
432+
@Test // DATAMONGO-1902
433+
void rendersProjectOnNestedPrefixedEmbeddedFieldCorrectly() {
434+
435+
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
436+
437+
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
438+
project().and("withEmbedded.prefixedEmbeddableValue.stringValue").as("val")).toDocument("collection", context);
439+
440+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
441+
.isEqualTo(new Document("val", "$withEmbedded.prefix-stringValue"));
442+
}
443+
444+
@Test // DATAMONGO-1902
445+
void rendersProjectOnNestedPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
446+
447+
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
448+
449+
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
450+
project().and("withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection",
451+
context);
452+
453+
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
454+
.isEqualTo(new Document("val", "$withEmbedded.prefix-with-at-field-annotation"));
455+
}
456+
360457
@org.springframework.data.mongodb.core.mapping.Document(collection = "person")
361458
@AllArgsConstructor
362459
public static class FooPerson {
@@ -433,4 +530,26 @@ static class Nested {
433530
String value1;
434531
@org.springframework.data.mongodb.core.mapping.Field("nestedValue2") String value2;
435532
}
533+
534+
static class WrapperAroundWithEmbedded {
535+
536+
String id;
537+
WithEmbedded withEmbedded;
538+
}
539+
540+
static class WithEmbedded {
541+
542+
String id;
543+
544+
@Embedded.Nullable EmbeddableType embeddableType;
545+
@Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue;
546+
}
547+
548+
static class EmbeddableType {
549+
550+
String stringValue;
551+
552+
@org.springframework.data.mongodb.core.mapping.Field("with-at-field-annotation") //
553+
String atFieldAnnotatedValue;
554+
}
436555
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,14 +1085,15 @@ void rendersQueryOnNestedPrefixedEmbeddedCorrectly() {
10851085
}
10861086

10871087
@Test // DATAMONGO-1902
1088-
void sortByEmbeddable() {
1088+
void sortByEmbeddableIsEmpty() {
10891089

10901090
Query query = new Query().with(Sort.by("embeddableValue"));
10911091

10921092
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
10931093
context.getPersistentEntity(WithEmbedded.class));
10941094

1095-
assertThat(document).isEqualTo(new org.bson.Document());
1095+
assertThat(document).isEqualTo(
1096+
new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
10961097
}
10971098

10981099
@Test // DATAMONGO-1902
@@ -1151,6 +1152,31 @@ void sortByNestedPrefixedEmbeddableValueWithFieldAnnotation() {
11511152
assertThat(document).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-with-at-field-annotation", 1));
11521153
}
11531154

1155+
@Test // DATAMONGO-1902
1156+
void projectOnEmbeddableUsesFields() {
1157+
1158+
Query query = new Query();
1159+
query.fields().include("embeddableValue");
1160+
1161+
org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
1162+
context.getPersistentEntity(WithEmbedded.class));
1163+
1164+
assertThat(document).isEqualTo(
1165+
new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
1166+
}
1167+
1168+
@Test // DATAMONGO-1902
1169+
void projectOnEmbeddableValue() {
1170+
1171+
Query query = new Query();
1172+
query.fields().include("embeddableValue.stringValue");
1173+
1174+
org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
1175+
context.getPersistentEntity(WithEmbedded.class));
1176+
1177+
assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1));
1178+
}
1179+
11541180
class WithDeepArrayNesting {
11551181

11561182
List<WithNestedArray> level0;

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.junit.runners.Suite;
3232
import org.junit.runners.Suite.SuiteClasses;
3333
import org.springframework.core.annotation.AliasFor;
34+
import org.springframework.dao.InvalidDataAccessApiUsageException;
3435
import org.springframework.data.annotation.Embedded;
3536
import org.springframework.data.annotation.Id;
3637
import org.springframework.data.geo.Point;
@@ -1314,6 +1315,14 @@ public void resolvedIndexOnNestedEmbeddedType() {
13141315
});
13151316
}
13161317

1318+
@Test // DATAMONGO-1902
1319+
public void errorsOnIndexOnEmbedded() {
1320+
1321+
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
1322+
.isThrownBy(() -> prepareMappingContextAndResolveIndexForType(InvalidIndexOnEmbedded.class));
1323+
1324+
}
1325+
13171326
@Document
13181327
class MixedIndexRoot {
13191328

@@ -1519,6 +1528,15 @@ static class WithEmbedded {
15191528
@Embedded.Nullable EmbeddableType embeddableType;
15201529
}
15211530

1531+
@Document
1532+
class InvalidIndexOnEmbedded {
1533+
1534+
@Indexed //
1535+
@Embedded.Nullable //
1536+
EmbeddableType embeddableType;
1537+
1538+
}
1539+
15221540
static class EmbeddableType {
15231541

15241542
@Indexed String stringValue;

0 commit comments

Comments
 (0)