Skip to content

Commit c8307d5

Browse files
christophstroblmp911de
authored andcommitted
Allow one-to-many style lookups with via @DocumentReference.
This commit adds support for relational style One-To-Many references using a combination of ReadonlyProperty and @DocumentReference. It allows to link types without explicitly storing the linking values within the document itself. @document class Publisher { @id ObjectId id; // ... @ReadOnlyProperty @DocumentReference(lookup="{'publisherId':?#{#self._id} }") List<Book> books; } Closes: #3798 Original pull request: #3802.
1 parent dcf1848 commit c8307d5

File tree

6 files changed

+218
-19
lines changed

6 files changed

+218
-19
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,6 @@ private Object createLazyLoadingProxy(MongoPersistentProperty property, Object s
108108
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
109109
return proxyFactory.createLazyLoadingProxy(property, it -> {
110110
return referenceLookupDelegate.readReference(it, source, lookupFunction, entityReader);
111-
}, source);
111+
}, source instanceof DocumentReferenceSource ? ((DocumentReferenceSource)source).getTargetSource() : source);
112112
}
113113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert;
17+
18+
import org.springframework.lang.Nullable;
19+
20+
/**
21+
* The source object to resolve document references upon. Encapsulates the actual source and the reference specific
22+
* values.
23+
*
24+
* @author Christoph Strobl
25+
* @since 3.3
26+
*/
27+
public class DocumentReferenceSource {
28+
29+
private final Object self;
30+
31+
@Nullable private final Object targetSource;
32+
33+
/**
34+
* Create a new instance of {@link DocumentReferenceSource}.
35+
*
36+
* @param self the entire wrapper object holding references. Must not be {@literal null}.
37+
* @param targetSource the reference value source.
38+
*/
39+
DocumentReferenceSource(Object self, @Nullable Object targetSource) {
40+
41+
this.self = self;
42+
this.targetSource = targetSource;
43+
}
44+
45+
/**
46+
* Get the outer document.
47+
*
48+
* @return never {@literal null}.
49+
*/
50+
public Object getSelf() {
51+
return self;
52+
}
53+
54+
/**
55+
* Get the actual (property specific) reference value.
56+
*
57+
* @return can be {@literal null}.
58+
*/
59+
@Nullable
60+
public Object getTargetSource() {
61+
return targetSource;
62+
}
63+
}

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

+10-6
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.bson.types.ObjectId;
3939
import org.slf4j.Logger;
4040
import org.slf4j.LoggerFactory;
41-
4241
import org.springframework.beans.BeansException;
4342
import org.springframework.context.ApplicationContext;
4443
import org.springframework.context.ApplicationContextAware;
@@ -524,28 +523,33 @@ private void readAssociation(Association<MongoPersistentProperty> association, P
524523
MongoPersistentProperty property = association.getInverse();
525524
Object value = documentAccessor.get(property);
526525

527-
if (value == null) {
528-
return;
529-
}
530-
531526
if (property.isDocumentReference()
532527
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) {
533528

534529
// quite unusual but sounds like worth having?
535530

536531
if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) {
537532

533+
if(value == null) {
534+
return;
535+
}
536+
538537
DocumentPointer<?> pointer = () -> value;
539538

540539
// collection like special treatment
541540
accessor.setProperty(property, conversionService.convert(pointer, property.getActualType()));
542541
} else {
542+
543543
accessor.setProperty(property,
544-
dbRefResolver.resolveReference(property, value, referenceLookupDelegate, context::convert));
544+
dbRefResolver.resolveReference(property, new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)), referenceLookupDelegate, context::convert));
545545
}
546546
return;
547547
}
548548

549+
if (value == null) {
550+
return;
551+
}
552+
549553
DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
550554

551555
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));

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

+40-12
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,20 @@ public ReferenceLookupDelegate(
8787
* Read the reference expressed by the given property.
8888
*
8989
* @param property the reference defining property. Must not be {@literal null}. THe
90-
* @param value the source value identifying to the referenced entity. Must not be {@literal null}.
90+
* @param source the source value identifying to the referenced entity. Must not be {@literal null}.
9191
* @param lookupFunction to execute a lookup query. Must not be {@literal null}.
9292
* @param entityReader the callback to convert raw source values into actual domain types. Must not be
9393
* {@literal null}.
9494
* @return can be {@literal null}.
9595
*/
9696
@Nullable
97-
public Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction,
97+
public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
9898
MongoEntityReader entityReader) {
9999

100-
DocumentReferenceQuery filter = computeFilter(property, value, spELContext);
100+
Object value = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
101+
: source;
102+
103+
DocumentReferenceQuery filter = computeFilter(property, source, spELContext);
101104
ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext);
102105

103106
Iterable<Document> result = lookupFunction.apply(filter, referenceCollection);
@@ -196,8 +199,16 @@ private <T> T parseValueOrGet(String value, ParameterBindingContext bindingConte
196199

197200
ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) {
198201

199-
return new ParameterBindingContext(valueProviderFor(source), spELContext.getParser(),
202+
ValueProvider valueProvider;
203+
if (source instanceof DocumentReferenceSource) {
204+
valueProvider = valueProviderFor(((DocumentReferenceSource) source).getTargetSource());
205+
} else {
206+
valueProvider = valueProviderFor(source);
207+
}
208+
209+
return new ParameterBindingContext(valueProvider, spELContext.getParser(),
200210
() -> evaluationContextFor(property, source, spELContext));
211+
201212
}
202213

203214
ValueProvider valueProviderFor(Object source) {
@@ -212,9 +223,18 @@ ValueProvider valueProviderFor(Object source) {
212223

213224
EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
214225

215-
EvaluationContext ctx = spELContext.getEvaluationContext(source);
216-
ctx.setVariable("target", source);
217-
ctx.setVariable(property.getName(), source);
226+
Object target = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
227+
: source;
228+
229+
if (target == null) {
230+
target = new Document();
231+
}
232+
233+
EvaluationContext ctx = spELContext.getEvaluationContext(target);
234+
ctx.setVariable("target", target);
235+
ctx.setVariable("self",
236+
source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getSelf() : source);
237+
ctx.setVariable(property.getName(), target);
218238

219239
return ctx;
220240
}
@@ -223,22 +243,30 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object
223243
* Compute the query to retrieve linked documents.
224244
*
225245
* @param property must not be {@literal null}.
226-
* @param value must not be {@literal null}.
246+
* @param source must not be {@literal null}.
227247
* @param spELContext must not be {@literal null}.
228248
* @return never {@literal null}.
229249
*/
230250
@SuppressWarnings("unchecked")
231-
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
251+
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) {
232252

233253
DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference()
234254
: ReferenceEmulatingDocumentReference.INSTANCE;
235255

236256
String lookup = documentReference.lookup();
237257

238-
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext),
258+
Object value = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
259+
: source;
260+
261+
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, source, spELContext),
239262
() -> new Document());
240263

241-
if (property.isCollectionLike() && value instanceof Collection) {
264+
if (property.isCollectionLike() && (value instanceof Collection || value == null)) {
265+
266+
if (value == null) {
267+
return new ListDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, source, spELContext)),
268+
sort);
269+
}
242270

243271
List<Document> ors = new ArrayList<>();
244272
for (Object entry : (Collection<Object>) value) {
@@ -263,7 +291,7 @@ DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object va
263291
return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap);
264292
}
265293

266-
return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
294+
return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, source, spELContext)), sort);
267295
}
268296

269297
enum ReferenceEmulatingDocumentReference implements DocumentReference {

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

+48
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.junit.jupiter.api.extension.ExtendWith;
4040
import org.springframework.core.convert.converter.Converter;
4141
import org.springframework.data.annotation.Id;
42+
import org.springframework.data.annotation.ReadOnlyProperty;
4243
import org.springframework.data.annotation.Reference;
4344
import org.springframework.data.convert.WritingConverter;
4445
import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils;
@@ -1049,7 +1050,34 @@ void updateWhenUsingAtReferenceDirectly() {
10491050
});
10501051

10511052
assertThat(target).containsEntry("publisher", "p-1");
1053+
}
1054+
1055+
@Test // GH-3798
1056+
void allowsOneToMayStyleLookupsUsingSelfVariable() {
1057+
1058+
OneToManyStyleBook book1 = new OneToManyStyleBook();
1059+
book1.id = "id-1";
1060+
book1.publisherId = "p-100";
1061+
1062+
OneToManyStyleBook book2 = new OneToManyStyleBook();
1063+
book2.id = "id-2";
1064+
book2.publisherId = "p-200";
1065+
1066+
OneToManyStyleBook book3 = new OneToManyStyleBook();
1067+
book3.id = "id-3";
1068+
book3.publisherId = "p-100";
1069+
1070+
template.save(book1);
1071+
template.save(book2);
1072+
template.save(book3);
10521073

1074+
OneToManyStylePublisher publisher = new OneToManyStylePublisher();
1075+
publisher.id = "p-100";
1076+
1077+
template.save(publisher);
1078+
1079+
OneToManyStylePublisher target = template.findOne(query(where("id").is(publisher.id)), OneToManyStylePublisher.class);
1080+
assertThat(target.books).containsExactlyInAnyOrder(book1, book3);
10531081
}
10541082

10551083
@Data
@@ -1293,4 +1321,24 @@ static class UsingAtReference {
12931321
@Reference //
12941322
Publisher publisher;
12951323
}
1324+
1325+
@Data
1326+
static class OneToManyStyleBook {
1327+
1328+
@Id
1329+
String id;
1330+
1331+
private String publisherId;
1332+
}
1333+
1334+
@Data
1335+
static class OneToManyStylePublisher {
1336+
1337+
@Id
1338+
String id;
1339+
1340+
@ReadOnlyProperty
1341+
@DocumentReference(lookup="{'publisherId':?#{#self._id} }")
1342+
List<OneToManyStyleBook> books;
1343+
}
12961344
}

src/main/asciidoc/reference/document-references.adoc

+56
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,62 @@ class Publisher {
262262
<2> The field value placeholders of the lookup query (like `acc`) is used to form the reference document.
263263
====
264264

265+
It is also possible to model relational style _One-To-Many_ references using a combination of `@ReadonlyProperty` and `@DocumentReference`.
266+
This approach allows to link types without explicitly storing the linking values within the document itself as shown in the snipped below.
267+
268+
====
269+
[source,java]
270+
----
271+
@Document
272+
class Book {
273+
274+
@Id
275+
ObjectId id;
276+
String title;
277+
List<String> author;
278+
279+
ObjectId publisherId; <1>
280+
}
281+
282+
@Document
283+
class Publisher {
284+
285+
@Id
286+
ObjectId id;
287+
String acronym;
288+
String name;
289+
290+
@ReadOnlyProperty <2>
291+
@DocumentReference(lookup="{'publisherId':?#{#self._id} }") <3>
292+
List<Book> books;
293+
}
294+
----
295+
296+
.`Book` document
297+
[source,json]
298+
----
299+
{
300+
"_id" : 9a48e32,
301+
"title" : "The Warded Man",
302+
"author" : ["Peter V. Brett"],
303+
"publisherId" : 8cfb002
304+
}
305+
----
306+
307+
.`Publisher` document
308+
[source,json]
309+
----
310+
{
311+
"_id" : 8cfb002,
312+
"acronym" : "DR",
313+
"name" : "Del Rey"
314+
}
315+
----
316+
<1> Set up the link from `Book` to `Publisher` by storing the `Publisher.id` within the `Book` document.
317+
<2> Mark the property holding the references to be read only. This prevents storing references to individual ``Book``s with the `Publisher` document.
318+
<3> Use the `#self` variable to access values within the `Publisher` document and in this retrieve `Books` with matching `publisherId`.
319+
====
320+
265321
With all the above in place it is possible to model all kind of associations between entities.
266322
Have a look at the non-exhaustive list of samples below to get feeling for what is possible.
267323

0 commit comments

Comments
 (0)