Skip to content

Commit 7c9eb26

Browse files
committed
Add support for keyset extraction of nested property paths.
Closes #4326
1 parent dd0735b commit 7c9eb26

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

Lines changed: 66 additions & 13 deletions
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
/**
@@ -382,6 +382,16 @@ interface Entity<T> {
382382
*/
383383
Object getId();
384384

385+
/**
386+
* Returns the property value for {@code key}.
387+
*
388+
* @param key
389+
* @return
390+
* @since 4.1
391+
*/
392+
@Nullable
393+
Object getPropertyValue(String key);
394+
385395
/**
386396
* Returns the {@link Query} to find the entity by its identifier.
387397
*
@@ -453,6 +463,11 @@ default boolean isVersionedEntity() {
453463
*/
454464
boolean isNew();
455465

466+
/**
467+
* @param sortObject
468+
* @return
469+
* @since 3.1
470+
*/
456471
Map<String, Object> extractKeys(Document sortObject);
457472

458473
}
@@ -514,7 +529,12 @@ public String getIdFieldName() {
514529

515530
@Override
516531
public Object getId() {
517-
return map.get(ID_FIELD);
532+
return getPropertyValue(ID_FIELD);
533+
}
534+
535+
@Override
536+
public Object getPropertyValue(String key) {
537+
return map.get(key);
518538
}
519539

520540
@Override
@@ -609,23 +629,26 @@ private static class MappedEntity<T> implements Entity<T> {
609629
private final MongoPersistentEntity<?> entity;
610630
private final IdentifierAccessor idAccessor;
611631
private final PersistentPropertyAccessor<T> propertyAccessor;
632+
private final EntityOperations entityOperations;
612633

613634
protected MappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor idAccessor,
614-
PersistentPropertyAccessor<T> propertyAccessor) {
635+
PersistentPropertyAccessor<T> propertyAccessor, EntityOperations entityOperations) {
615636

616637
this.entity = entity;
617638
this.idAccessor = idAccessor;
618639
this.propertyAccessor = propertyAccessor;
640+
this.entityOperations = entityOperations;
619641
}
620642

621643
private static <T> MappedEntity<T> of(T bean,
622-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
644+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
645+
EntityOperations entityOperations) {
623646

624647
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
625648
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
626649
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
627650

628-
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor);
651+
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor, entityOperations);
629652
}
630653

631654
@Override
@@ -638,6 +661,11 @@ public Object getId() {
638661
return idAccessor.getRequiredIdentifier();
639662
}
640663

664+
@Override
665+
public Object getPropertyValue(String key) {
666+
return propertyAccessor.getProperty(entity.getRequiredPersistentProperty(key));
667+
}
668+
641669
@Override
642670
public Query getByIdQuery() {
643671

@@ -724,13 +752,38 @@ public Map<String, Object> extractKeys(Document sortObject) {
724752

725753
for (String key : sortObject.keySet()) {
726754

727-
// TODO: make this work for nested properties
728-
MongoPersistentProperty persistentProperty = entity.getRequiredPersistentProperty(key);
729-
keyset.put(key, propertyAccessor.getProperty(persistentProperty));
755+
if (key.indexOf('.') != -1) {
756+
757+
// follow the path across nested levels.
758+
// TODO: We should have a MongoDB-specific property path abstraction to allow diving into Document.
759+
keyset.put(key, getNestedPropertyValue(key));
760+
} else {
761+
keyset.put(key, getPropertyValue(key));
762+
}
730763
}
731764

732765
return keyset;
733766
}
767+
768+
@Nullable
769+
private Object getNestedPropertyValue(String key) {
770+
771+
String[] segments = key.split("\\.");
772+
Entity<?> currentEntity = this;
773+
Object currentValue = null;
774+
775+
for (int i = 0; i < segments.length; i++) {
776+
777+
String segment = segments[i];
778+
currentValue = currentEntity.getPropertyValue(segment);
779+
780+
if (i < segments.length - 1) {
781+
currentEntity = entityOperations.forEntity(currentValue);
782+
}
783+
}
784+
785+
return currentValue;
786+
}
734787
}
735788

736789
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
@@ -740,9 +793,9 @@ private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements
740793
private final IdentifierAccessor identifierAccessor;
741794

742795
private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor identifierAccessor,
743-
ConvertingPropertyAccessor<T> propertyAccessor) {
796+
ConvertingPropertyAccessor<T> propertyAccessor, EntityOperations entityOperations) {
744797

745-
super(entity, identifierAccessor, propertyAccessor);
798+
super(entity, identifierAccessor, propertyAccessor, entityOperations);
746799

747800
this.entity = entity;
748801
this.propertyAccessor = propertyAccessor;
@@ -751,14 +804,14 @@ private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccesso
751804

752805
private static <T> AdaptibleEntity<T> of(T bean,
753806
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
754-
ConversionService conversionService) {
807+
ConversionService conversionService, EntityOperations entityOperations) {
755808

756809
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
757810
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
758811
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
759812

760813
return new AdaptibleMappedEntity<>(entity, identifierAccessor,
761-
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService));
814+
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), entityOperations);
762815
}
763816

764817
@Nullable

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

Lines changed: 71 additions & 1 deletion
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

Lines changed: 66 additions & 1 deletion
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)