Skip to content

Commit d8c04f0

Browse files
Use projecting read callback to allow interface projections.
Along the lines fix entity operations proxy handling by reading the underlying map instead of inspecting the proxy interface. Also make sure to map potential raw fields back to the according property. See: #4308 Original Pull Request: #4317
1 parent 85826e1 commit d8c04f0

File tree

7 files changed

+382
-91
lines changed

7 files changed

+382
-91
lines changed

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

+62-15
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import org.springframework.data.mapping.MappingException;
3131
import org.springframework.data.mapping.PersistentEntity;
3232
import org.springframework.data.mapping.PersistentPropertyAccessor;
33+
import org.springframework.data.mapping.PersistentPropertyPath;
34+
import org.springframework.data.mapping.PropertyPath;
3335
import org.springframework.data.mapping.context.MappingContext;
3436
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
3537
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
@@ -50,6 +52,7 @@
5052
import org.springframework.data.projection.EntityProjection;
5153
import org.springframework.data.projection.EntityProjectionIntrospector;
5254
import org.springframework.data.projection.ProjectionFactory;
55+
import org.springframework.data.projection.TargetAware;
5356
import org.springframework.data.util.Optionals;
5457
import org.springframework.lang.Nullable;
5558
import org.springframework.util.Assert;
@@ -117,12 +120,16 @@ <T> Entity<T> forEntity(T entity) {
117120

118121
Assert.notNull(entity, "Bean must not be null");
119122

123+
if (entity instanceof TargetAware targetAware) {
124+
return new SimpleMappedEntity((Map<String, Object>) targetAware.getTarget(), this);
125+
}
126+
120127
if (entity instanceof String) {
121-
return new UnmappedEntity(parse(entity.toString()));
128+
return new UnmappedEntity(parse(entity.toString()), this);
122129
}
123130

124131
if (entity instanceof Map) {
125-
return new SimpleMappedEntity((Map<String, Object>) entity);
132+
return new SimpleMappedEntity((Map<String, Object>) entity, this);
126133
}
127134

128135
return MappedEntity.of(entity, context, this);
@@ -142,11 +149,11 @@ <T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService)
142149
Assert.notNull(conversionService, "ConversionService must not be null");
143150

144151
if (entity instanceof String) {
145-
return new UnmappedEntity(parse(entity.toString()));
152+
return new UnmappedEntity(parse(entity.toString()), this);
146153
}
147154

148155
if (entity instanceof Map) {
149-
return new SimpleMappedEntity((Map<String, Object>) entity);
156+
return new SimpleMappedEntity((Map<String, Object>) entity, this);
150157
}
151158

152159
return AdaptibleMappedEntity.of(entity, context, conversionService, this);
@@ -287,7 +294,8 @@ public <T> TypedOperations<T> forType(@Nullable Class<T> entityClass) {
287294
*/
288295
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> entityType) {
289296

290-
if (!queryMapper.getMappingContext().hasPersistentEntityFor(entityType)) {
297+
MongoPersistentEntity<?> persistentEntity = queryMapper.getMappingContext().getPersistentEntity(entityType);
298+
if (persistentEntity == null && !resultType.isInterface() || ClassUtils.isAssignable(Document.class, resultType)) {
291299
return (EntityProjection) EntityProjection.nonProjecting(resultType);
292300
}
293301
return introspector.introspect(resultType, entityType);
@@ -369,6 +377,7 @@ private Document getMappedValidator(Validator validator, Class<?> domainType) {
369377
* A representation of information about an entity.
370378
*
371379
* @author Oliver Gierke
380+
* @author Christoph Strobl
372381
* @since 2.1
373382
*/
374383
interface Entity<T> {
@@ -471,10 +480,10 @@ default boolean isVersionedEntity() {
471480
/**
472481
* @param sortObject
473482
* @return
474-
* @since 3.1
483+
* @since 4.1
475484
* @throws IllegalStateException if a sort key yields {@literal null}.
476485
*/
477-
Map<String, Object> extractKeys(Document sortObject);
486+
Map<String, Object> extractKeys(Document sortObject, Class<?> sourceType);
478487

479488
}
480489

@@ -523,9 +532,11 @@ interface AdaptibleEntity<T> extends Entity<T> {
523532
private static class UnmappedEntity<T extends Map<String, Object>> implements AdaptibleEntity<T> {
524533

525534
private final T map;
535+
private final EntityOperations entityOperations;
526536

527-
protected UnmappedEntity(T map) {
537+
protected UnmappedEntity(T map, EntityOperations entityOperations) {
528538
this.map = map;
539+
this.entityOperations = entityOperations;
529540
}
530541

531542
@Override
@@ -596,13 +607,19 @@ public boolean isNew() {
596607
}
597608

598609
@Override
599-
public Map<String, Object> extractKeys(Document sortObject) {
610+
public Map<String, Object> extractKeys(Document sortObject, Class<?> sourceType) {
600611

601612
Map<String, Object> keyset = new LinkedHashMap<>();
602-
keyset.put(ID_FIELD, getId());
613+
MongoPersistentEntity<?> sourceEntity = entityOperations.context.getPersistentEntity(sourceType);
614+
if (sourceEntity != null && sourceEntity.hasIdProperty()) {
615+
keyset.put(sourceEntity.getRequiredIdProperty().getName(), getId());
616+
} else {
617+
keyset.put(ID_FIELD, getId());
618+
}
603619

604620
for (String key : sortObject.keySet()) {
605-
Object value = BsonUtils.resolveValue(map, key);
621+
622+
Object value = resolveValue(key, sourceEntity);
606623

607624
if (value == null) {
608625
throw new IllegalStateException(
@@ -614,12 +631,24 @@ public Map<String, Object> extractKeys(Document sortObject) {
614631

615632
return keyset;
616633
}
634+
635+
@Nullable
636+
private Object resolveValue(String key, @Nullable MongoPersistentEntity<?> sourceEntity) {
637+
638+
if (sourceEntity == null) {
639+
return BsonUtils.resolveValue(map, key);
640+
}
641+
PropertyPath from = PropertyPath.from(key, sourceEntity.getTypeInformation());
642+
PersistentPropertyPath<MongoPersistentProperty> persistentPropertyPath = entityOperations.context
643+
.getPersistentPropertyPath(from);
644+
return BsonUtils.resolveValue(map, persistentPropertyPath.toDotPath(p -> p.getFieldName()));
645+
}
617646
}
618647

619648
private static class SimpleMappedEntity<T extends Map<String, Object>> extends UnmappedEntity<T> {
620649

621-
protected SimpleMappedEntity(T map) {
622-
super(map);
650+
protected SimpleMappedEntity(T map, EntityOperations entityOperations) {
651+
super(map, entityOperations);
623652
}
624653

625654
@Override
@@ -758,10 +787,15 @@ public boolean isNew() {
758787
}
759788

760789
@Override
761-
public Map<String, Object> extractKeys(Document sortObject) {
790+
public Map<String, Object> extractKeys(Document sortObject, Class<?> sourceType) {
762791

763792
Map<String, Object> keyset = new LinkedHashMap<>();
764-
keyset.put(entity.getRequiredIdProperty().getName(), getId());
793+
MongoPersistentEntity<?> sourceEntity = entityOperations.context.getPersistentEntity(sourceType);
794+
if (sourceEntity != null && sourceEntity.hasIdProperty()) {
795+
keyset.put(sourceEntity.getRequiredIdProperty().getName(), getId());
796+
} else {
797+
keyset.put(entity.getRequiredIdProperty().getName(), getId());
798+
}
765799

766800
for (String key : sortObject.keySet()) {
767801

@@ -933,6 +967,14 @@ interface TypedOperations<T> {
933967
* @since 3.3
934968
*/
935969
TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options);
970+
971+
/**
972+
* @return the name of the id field.
973+
* @since 4.1
974+
*/
975+
default String getIdKeyName() {
976+
return ID_FIELD;
977+
}
936978
}
937979

938980
/**
@@ -1055,6 +1097,11 @@ private String mappedNameOrDefault(String name) {
10551097
MongoPersistentProperty persistentProperty = entity.getPersistentProperty(name);
10561098
return persistentProperty != null ? persistentProperty.getFieldName() : name;
10571099
}
1100+
1101+
@Override
1102+
public String getIdKeyName() {
1103+
return entity.getIdProperty().getName();
1104+
}
10581105
}
10591106

10601107
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,8 @@ <T> Window<T> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
870870
Assert.notNull(sourceClass, "Entity type must not be null");
871871
Assert.notNull(targetClass, "Target type must not be null");
872872

873-
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(mongoConverter, targetClass, collectionName);
873+
EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
874+
ProjectingReadCallback<?,T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
874875
int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
875876

876877
if (query.hasKeyset()) {
@@ -882,7 +883,7 @@ <T> Window<T> doScroll(Query query, Class<?> sourceClass, Class<T> targetClass,
882883
keysetPaginationQuery.fields(), sourceClass,
883884
new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback);
884885

885-
return ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), result, operations);
886+
return ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), result, sourceClass, operations);
886887
}
887888

888889
List<T> result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(),

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,8 @@ <T> Mono<Window<T>> doScroll(Query query, Class<?> sourceClass, Class<T> targetC
849849
Assert.notNull(sourceClass, "Entity type must not be null");
850850
Assert.notNull(targetClass, "Target type must not be null");
851851

852+
EntityProjection<T, ?> projection = operations.introspectProjection(targetClass, sourceClass);
853+
ProjectingReadCallback<?,T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
852854
int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
853855

854856
if (query.hasKeyset()) {
@@ -857,15 +859,15 @@ <T> Mono<Window<T>> doScroll(Query query, Class<?> sourceClass, Class<T> targetC
857859
operations.getIdPropertyName(sourceClass));
858860

859861
Mono<List<T>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query),
860-
keysetPaginationQuery.query(), keysetPaginationQuery.fields(), targetClass,
861-
new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass)).collectList();
862+
keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass,
863+
new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback).collectList();
862864

863-
return result.map(it -> ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), it, operations));
865+
return result.map(it -> ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), it, sourceClass, operations));
864866
}
865867

866868
Mono<List<T>> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
867-
query.getFieldsObject(), targetClass,
868-
new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass))
869+
query.getFieldsObject(), sourceClass,
870+
new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback)
869871
.collectList();
870872

871873
return result.map(

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,15 @@ private static String getComparator(int sortOrder, Direction direction) {
121121
return sortOrder == 1 ? "$gt" : "$lt";
122122
}
123123

124-
static <T> Window<T> createWindow(Document sortObject, int limit, List<T> result, EntityOperations operations) {
124+
static <T> Window<T> createWindow(Document sortObject, int limit, List<T> result, Class<?> sourceType,
125+
EntityOperations operations) {
125126

126127
IntFunction<KeysetScrollPosition> positionFunction = value -> {
127128

128129
T last = result.get(value);
129130
Entity<T> entity = operations.forEntity(last);
130131

131-
Map<String, Object> keys = entity.extractKeys(sortObject);
132+
Map<String, Object> keys = entity.extractKeys(sortObject, sourceType);
132133
return KeysetScrollPosition.of(keys);
133134
};
134135

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

+35-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@
3333
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
3434
import org.springframework.data.mongodb.core.mapping.TimeSeries;
3535
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
36+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3637

3738
/**
3839
* Unit tests for {@link EntityOperations}.
3940
*
4041
* @author Mark Paluch
42+
* @author Christoph Strobl
4143
*/
4244
class EntityOperationsUnitTests {
4345

@@ -70,7 +72,8 @@ void shouldExtractKeysFromEntity() {
7072

7173
WithNestedDocument object = new WithNestedDocument("foo");
7274

73-
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1));
75+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1),
76+
WithNestedDocument.class);
7477

7578
assertThat(keys).containsEntry("id", "foo");
7679
}
@@ -80,7 +83,7 @@ void shouldExtractKeysFromDocument() {
8083

8184
Document object = new Document("id", "foo");
8285

83-
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1));
86+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1), Document.class);
8487

8588
assertThat(keys).containsEntry("id", "foo");
8689
}
@@ -90,7 +93,8 @@ void shouldExtractKeysFromNestedEntity() {
9093

9194
WithNestedDocument object = new WithNestedDocument("foo", new WithNestedDocument("bar"), null);
9295

93-
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("nested.id", 1));
96+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("nested.id", 1),
97+
WithNestedDocument.class);
9498

9599
assertThat(keys).containsEntry("nested.id", "bar");
96100
}
@@ -101,7 +105,8 @@ void shouldExtractKeysFromNestedEntityDocument() {
101105
WithNestedDocument object = new WithNestedDocument("foo", new WithNestedDocument("bar"),
102106
new Document("john", "doe"));
103107

104-
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1));
108+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1),
109+
WithNestedDocument.class);
105110

106111
assertThat(keys).containsEntry("document.john", "doe");
107112
}
@@ -111,11 +116,32 @@ void shouldExtractKeysFromNestedDocument() {
111116

112117
Document object = new Document("document", new Document("john", "doe"));
113118

114-
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1));
119+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1),
120+
Document.class);
115121

116122
assertThat(keys).containsEntry("document.john", "doe");
117123
}
118124

125+
@Test // GH-4308
126+
void shouldExtractIdPropertyNameFromRawDocument() {
127+
128+
Document object = new Document("_id", "id-1").append("value", "val");
129+
130+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("value", 1), DomainTypeWithIdProperty.class);
131+
132+
assertThat(keys).containsEntry("id", "id-1");
133+
}
134+
135+
@Test // GH-4308
136+
void shouldExtractValuesFromProxy() {
137+
138+
ProjectionInterface source = new SpelAwareProxyProjectionFactory().createProjection(ProjectionInterface.class, new Document("_id", "id-1").append("value", "val"));
139+
140+
Map<String, Object> keys = operations.forEntity(source).extractKeys(new Document("value", 1), DomainTypeWithIdProperty.class);
141+
142+
assertThat(keys).isEqualTo(new Document("id", "id-1").append("value", "val"));
143+
}
144+
119145
<T> EntityOperations.AdaptibleEntity<T> initAdaptibleEntity(T source) {
120146
return operations.forEntity(source, conversionService);
121147
}
@@ -150,4 +176,8 @@ public WithNestedDocument(String id) {
150176
this.id = id;
151177
}
152178
}
179+
180+
interface ProjectionInterface {
181+
String getValue();
182+
}
153183
}

0 commit comments

Comments
 (0)