Skip to content

Commit eaa6393

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for keyset extraction of nested property paths.
Closes #4326 Original Pull Request: #4317
1 parent 7d485d7 commit eaa6393

File tree

3 files changed

+203
-15
lines changed

3 files changed

+203
-15
lines changed

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

+66-13
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ <T> Entity<T> forEntity(T entity) {
124124
return new SimpleMappedEntity((Map<String, Object>) entity);
125125
}
126126

127-
return MappedEntity.of(entity, context);
127+
return MappedEntity.of(entity, context, this);
128128
}
129129

130130
/**
@@ -148,7 +148,7 @@ <T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService)
148148
return new SimpleMappedEntity((Map<String, Object>) entity);
149149
}
150150

151-
return AdaptibleMappedEntity.of(entity, context, conversionService);
151+
return AdaptibleMappedEntity.of(entity, context, conversionService, this);
152152
}
153153

154154
/**
@@ -386,6 +386,16 @@ interface Entity<T> {
386386
*/
387387
Object getId();
388388

389+
/**
390+
* Returns the property value for {@code key}.
391+
*
392+
* @param key
393+
* @return
394+
* @since 4.1
395+
*/
396+
@Nullable
397+
Object getPropertyValue(String key);
398+
389399
/**
390400
* Returns the {@link Query} to find the entity by its identifier.
391401
*
@@ -457,6 +467,11 @@ default boolean isVersionedEntity() {
457467
*/
458468
boolean isNew();
459469

470+
/**
471+
* @param sortObject
472+
* @return
473+
* @since 3.1
474+
*/
460475
Map<String, Object> extractKeys(Document sortObject);
461476

462477
}
@@ -518,7 +533,12 @@ public String getIdFieldName() {
518533

519534
@Override
520535
public Object getId() {
521-
return map.get(ID_FIELD);
536+
return getPropertyValue(ID_FIELD);
537+
}
538+
539+
@Override
540+
public Object getPropertyValue(String key) {
541+
return map.get(key);
522542
}
523543

524544
@Override
@@ -613,23 +633,26 @@ private static class MappedEntity<T> implements Entity<T> {
613633
private final MongoPersistentEntity<?> entity;
614634
private final IdentifierAccessor idAccessor;
615635
private final PersistentPropertyAccessor<T> propertyAccessor;
636+
private final EntityOperations entityOperations;
616637

617638
protected MappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor idAccessor,
618-
PersistentPropertyAccessor<T> propertyAccessor) {
639+
PersistentPropertyAccessor<T> propertyAccessor, EntityOperations entityOperations) {
619640

620641
this.entity = entity;
621642
this.idAccessor = idAccessor;
622643
this.propertyAccessor = propertyAccessor;
644+
this.entityOperations = entityOperations;
623645
}
624646

625647
private static <T> MappedEntity<T> of(T bean,
626-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
648+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
649+
EntityOperations entityOperations) {
627650

628651
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
629652
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
630653
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
631654

632-
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor);
655+
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor, entityOperations);
633656
}
634657

635658
@Override
@@ -642,6 +665,11 @@ public Object getId() {
642665
return idAccessor.getRequiredIdentifier();
643666
}
644667

668+
@Override
669+
public Object getPropertyValue(String key) {
670+
return propertyAccessor.getProperty(entity.getRequiredPersistentProperty(key));
671+
}
672+
645673
@Override
646674
public Query getByIdQuery() {
647675

@@ -728,13 +756,38 @@ public Map<String, Object> extractKeys(Document sortObject) {
728756

729757
for (String key : sortObject.keySet()) {
730758

731-
// TODO: make this work for nested properties
732-
MongoPersistentProperty persistentProperty = entity.getRequiredPersistentProperty(key);
733-
keyset.put(key, propertyAccessor.getProperty(persistentProperty));
759+
if (key.indexOf('.') != -1) {
760+
761+
// follow the path across nested levels.
762+
// TODO: We should have a MongoDB-specific property path abstraction to allow diving into Document.
763+
keyset.put(key, getNestedPropertyValue(key));
764+
} else {
765+
keyset.put(key, getPropertyValue(key));
766+
}
734767
}
735768

736769
return keyset;
737770
}
771+
772+
@Nullable
773+
private Object getNestedPropertyValue(String key) {
774+
775+
String[] segments = key.split("\\.");
776+
Entity<?> currentEntity = this;
777+
Object currentValue = null;
778+
779+
for (int i = 0; i < segments.length; i++) {
780+
781+
String segment = segments[i];
782+
currentValue = currentEntity.getPropertyValue(segment);
783+
784+
if (i < segments.length - 1) {
785+
currentEntity = entityOperations.forEntity(currentValue);
786+
}
787+
}
788+
789+
return currentValue;
790+
}
738791
}
739792

740793
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
@@ -744,9 +797,9 @@ private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements
744797
private final IdentifierAccessor identifierAccessor;
745798

746799
private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor identifierAccessor,
747-
ConvertingPropertyAccessor<T> propertyAccessor) {
800+
ConvertingPropertyAccessor<T> propertyAccessor, EntityOperations entityOperations) {
748801

749-
super(entity, identifierAccessor, propertyAccessor);
802+
super(entity, identifierAccessor, propertyAccessor, entityOperations);
750803

751804
this.entity = entity;
752805
this.propertyAccessor = propertyAccessor;
@@ -755,14 +808,14 @@ private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccesso
755808

756809
private static <T> AdaptibleEntity<T> of(T bean,
757810
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
758-
ConversionService conversionService) {
811+
ConversionService conversionService, EntityOperations entityOperations) {
759812

760813
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
761814
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
762815
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
763816

764817
return new AdaptibleMappedEntity<>(entity, identifierAccessor,
765-
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService));
818+
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), entityOperations);
766819
}
767820

768821
@Nullable

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

+71-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import lombok.AllArgsConstructor;
21+
import lombok.NoArgsConstructor;
22+
2023
import java.time.Instant;
24+
import java.util.Map;
2125

26+
import org.bson.Document;
2227
import org.junit.jupiter.api.Test;
23-
2428
import org.springframework.core.convert.ConversionService;
2529
import org.springframework.core.convert.support.DefaultConversionService;
2630
import org.springframework.data.annotation.Id;
@@ -61,6 +65,57 @@ void populateIdShouldReturnTargetBeanWhenIdIsNull() {
6165
assertThat(initAdaptibleEntity(new DomainTypeWithIdProperty()).populateIdIfNecessary(null)).isNotNull();
6266
}
6367

68+
@Test // GH-4308
69+
void shouldExtractKeysFromEntity() {
70+
71+
WithNestedDocument object = new WithNestedDocument("foo");
72+
73+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1));
74+
75+
assertThat(keys).containsEntry("id", "foo");
76+
}
77+
78+
@Test // GH-4308
79+
void shouldExtractKeysFromDocument() {
80+
81+
Document object = new Document("id", "foo");
82+
83+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("id", 1));
84+
85+
assertThat(keys).containsEntry("id", "foo");
86+
}
87+
88+
@Test // GH-4308
89+
void shouldExtractKeysFromNestedEntity() {
90+
91+
WithNestedDocument object = new WithNestedDocument("foo", new WithNestedDocument("bar"), null);
92+
93+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("nested.id", 1));
94+
95+
assertThat(keys).containsEntry("nested.id", "bar");
96+
}
97+
98+
@Test // GH-4308
99+
void shouldExtractKeysFromNestedEntityDocument() {
100+
101+
WithNestedDocument object = new WithNestedDocument("foo", new WithNestedDocument("bar"),
102+
new Document("john", "doe"));
103+
104+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1));
105+
106+
assertThat(keys).containsEntry("document.john", "doe");
107+
}
108+
109+
@Test // GH-4308
110+
void shouldExtractKeysFromNestedDocument() {
111+
112+
Document object = new Document("document", new Document("john", "doe"));
113+
114+
Map<String, Object> keys = operations.forEntity(object).extractKeys(new Document("document.john", 1));
115+
116+
assertThat(keys).containsEntry("document.john", "doe");
117+
}
118+
64119
<T> EntityOperations.AdaptibleEntity<T> initAdaptibleEntity(T source) {
65120
return operations.forEntity(source, conversionService);
66121
}
@@ -80,4 +135,19 @@ static class InvalidTimeField {
80135
static class InvalidMetaField {
81136
Instant time;
82137
}
138+
139+
@AllArgsConstructor
140+
@NoArgsConstructor
141+
class WithNestedDocument {
142+
143+
String id;
144+
145+
WithNestedDocument nested;
146+
147+
Document document;
148+
149+
public WithNestedDocument(String id) {
150+
this.id = id;
151+
}
152+
}
83153
}

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

+66-1
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@
1818
import static org.springframework.data.mongodb.core.query.Criteria.*;
1919
import static org.springframework.data.mongodb.test.util.Assertions.*;
2020

21+
import lombok.Data;
22+
import lombok.NoArgsConstructor;
23+
2124
import java.util.Arrays;
2225
import java.util.function.Function;
2326
import java.util.stream.Stream;
2427

2528
import org.bson.Document;
2629
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
2731
import org.junit.jupiter.api.extension.ExtendWith;
2832
import org.junit.jupiter.params.ParameterizedTest;
2933
import org.junit.jupiter.params.provider.Arguments;
3034
import org.junit.jupiter.params.provider.MethodSource;
3135
import org.springframework.context.ConfigurableApplicationContext;
3236
import org.springframework.context.support.GenericApplicationContext;
37+
import org.springframework.data.annotation.PersistenceCreator;
3338
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
3439
import org.springframework.data.domain.KeysetScrollPosition;
3540
import org.springframework.data.domain.OffsetScrollPosition;
@@ -69,7 +74,6 @@ class MongoTemplateScrollTests {
6974

7075
cfg.configureMappingContext(it -> {
7176
it.autocreateIndex(false);
72-
it.initialEntitySet(AuditablePerson.class);
7377
});
7478

7579
cfg.configureApplicationContext(it -> {
@@ -87,6 +91,39 @@ class MongoTemplateScrollTests {
8791
@BeforeEach
8892
void setUp() {
8993
template.remove(Person.class).all();
94+
template.remove(WithNestedDocument.class).all();
95+
}
96+
97+
@Test
98+
void shouldUseKeysetScrollingWithNestedSort() {
99+
100+
WithNestedDocument john20 = new WithNestedDocument(null, "John", 120, new WithNestedDocument("John", 20),
101+
new Document("name", "bar"));
102+
WithNestedDocument john40 = new WithNestedDocument(null, "John", 140, new WithNestedDocument("John", 40),
103+
new Document("name", "baz"));
104+
WithNestedDocument john41 = new WithNestedDocument(null, "John", 141, new WithNestedDocument("John", 41),
105+
new Document("name", "foo"));
106+
107+
template.insertAll(Arrays.asList(john20, john40, john41));
108+
109+
Query q = new Query(where("name").regex("J.*")).with(Sort.by("nested.name", "nested.age", "document.name"))
110+
.limit(2);
111+
q.with(KeysetScrollPosition.initial());
112+
113+
Scroll<WithNestedDocument> scroll = template.scroll(q, WithNestedDocument.class);
114+
115+
assertThat(scroll.hasNext()).isTrue();
116+
assertThat(scroll.isLast()).isFalse();
117+
assertThat(scroll).hasSize(2);
118+
assertThat(scroll).containsOnly(john20, john40);
119+
120+
scroll = template.scroll(q.with(scroll.lastPosition()), WithNestedDocument.class);
121+
122+
assertThat(scroll.hasNext()).isFalse();
123+
assertThat(scroll.isLast()).isTrue();
124+
assertThat(scroll).hasSize(1);
125+
assertThat(scroll).containsOnly(john41);
126+
90127
}
91128

92129
@ParameterizedTest // GH-4308
@@ -144,4 +181,32 @@ static Document toDocument(Person person) {
144181
return new Document("_class", person.getClass().getName()).append("_id", person.getId()).append("active", true)
145182
.append("firstName", person.getFirstName()).append("age", person.getAge());
146183
}
184+
185+
@NoArgsConstructor
186+
@Data
187+
class WithNestedDocument {
188+
189+
String id;
190+
String name;
191+
192+
int age;
193+
194+
WithNestedDocument nested;
195+
196+
Document document;
197+
198+
public WithNestedDocument(String name, int age) {
199+
this.name = name;
200+
this.age = age;
201+
}
202+
203+
@PersistenceCreator
204+
public WithNestedDocument(String id, String name, int age, WithNestedDocument nested, Document document) {
205+
this.id = id;
206+
this.name = name;
207+
this.age = age;
208+
this.nested = nested;
209+
this.document = document;
210+
}
211+
}
147212
}

0 commit comments

Comments
 (0)