Browse Source

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.
pull/3821/head
Christoph Strobl 4 years ago committed by Mark Paluch
parent
commit
c8307d5a39
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java
  2. 63
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java
  3. 16
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  4. 52
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java
  5. 48
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java
  6. 56
      src/main/asciidoc/reference/document-references.adoc

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

@ -108,6 +108,6 @@ public class DefaultReferenceResolver implements ReferenceResolver {
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) { ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
return proxyFactory.createLazyLoadingProxy(property, it -> { return proxyFactory.createLazyLoadingProxy(property, it -> {
return referenceLookupDelegate.readReference(it, source, lookupFunction, entityReader); return referenceLookupDelegate.readReference(it, source, lookupFunction, entityReader);
}, source); }, source instanceof DocumentReferenceSource ? ((DocumentReferenceSource)source).getTargetSource() : source);
} }
} }

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

@ -0,0 +1,63 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.convert;
import org.springframework.lang.Nullable;
/**
* The source object to resolve document references upon. Encapsulates the actual source and the reference specific
* values.
*
* @author Christoph Strobl
* @since 3.3
*/
public class DocumentReferenceSource {
private final Object self;
@Nullable private final Object targetSource;
/**
* Create a new instance of {@link DocumentReferenceSource}.
*
* @param self the entire wrapper object holding references. Must not be {@literal null}.
* @param targetSource the reference value source.
*/
DocumentReferenceSource(Object self, @Nullable Object targetSource) {
this.self = self;
this.targetSource = targetSource;
}
/**
* Get the outer document.
*
* @return never {@literal null}.
*/
public Object getSelf() {
return self;
}
/**
* Get the actual (property specific) reference value.
*
* @return can be {@literal null}.
*/
@Nullable
public Object getTargetSource() {
return targetSource;
}
}

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

@ -38,7 +38,6 @@ import org.bson.json.JsonReader;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
@ -524,10 +523,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
MongoPersistentProperty property = association.getInverse(); MongoPersistentProperty property = association.getInverse();
Object value = documentAccessor.get(property); Object value = documentAccessor.get(property);
if (value == null) {
return;
}
if (property.isDocumentReference() if (property.isDocumentReference()
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) { || (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) {
@ -535,17 +530,26 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) { if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) {
if(value == null) {
return;
}
DocumentPointer<?> pointer = () -> value; DocumentPointer<?> pointer = () -> value;
// collection like special treatment // collection like special treatment
accessor.setProperty(property, conversionService.convert(pointer, property.getActualType())); accessor.setProperty(property, conversionService.convert(pointer, property.getActualType()));
} else { } else {
accessor.setProperty(property, accessor.setProperty(property,
dbRefResolver.resolveReference(property, value, referenceLookupDelegate, context::convert)); dbRefResolver.resolveReference(property, new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)), referenceLookupDelegate, context::convert));
} }
return; return;
} }
if (value == null) {
return;
}
DBRef dbref = value instanceof DBRef ? (DBRef) value : null; DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler)); accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));

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

@ -87,17 +87,20 @@ public final class ReferenceLookupDelegate {
* Read the reference expressed by the given property. * Read the reference expressed by the given property.
* *
* @param property the reference defining property. Must not be {@literal null}. THe * @param property the reference defining property. Must not be {@literal null}. THe
* @param value the source value identifying to the referenced entity. Must not be {@literal null}. * @param source the source value identifying to the referenced entity. Must not be {@literal null}.
* @param lookupFunction to execute a lookup query. Must not be {@literal null}. * @param lookupFunction to execute a lookup query. Must not be {@literal null}.
* @param entityReader the callback to convert raw source values into actual domain types. Must not be * @param entityReader the callback to convert raw source values into actual domain types. Must not be
* {@literal null}. * {@literal null}.
* @return can be {@literal null}. * @return can be {@literal null}.
*/ */
@Nullable @Nullable
public Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
MongoEntityReader entityReader) { MongoEntityReader entityReader) {
DocumentReferenceQuery filter = computeFilter(property, value, spELContext); Object value = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
: source;
DocumentReferenceQuery filter = computeFilter(property, source, spELContext);
ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext); ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext);
Iterable<Document> result = lookupFunction.apply(filter, referenceCollection); Iterable<Document> result = lookupFunction.apply(filter, referenceCollection);
@ -196,8 +199,16 @@ public final class ReferenceLookupDelegate {
ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) { ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) {
return new ParameterBindingContext(valueProviderFor(source), spELContext.getParser(), ValueProvider valueProvider;
if (source instanceof DocumentReferenceSource) {
valueProvider = valueProviderFor(((DocumentReferenceSource) source).getTargetSource());
} else {
valueProvider = valueProviderFor(source);
}
return new ParameterBindingContext(valueProvider, spELContext.getParser(),
() -> evaluationContextFor(property, source, spELContext)); () -> evaluationContextFor(property, source, spELContext));
} }
ValueProvider valueProviderFor(Object source) { ValueProvider valueProviderFor(Object source) {
@ -212,9 +223,18 @@ public final class ReferenceLookupDelegate {
EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) { EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
EvaluationContext ctx = spELContext.getEvaluationContext(source); Object target = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
ctx.setVariable("target", source); : source;
ctx.setVariable(property.getName(), source);
if (target == null) {
target = new Document();
}
EvaluationContext ctx = spELContext.getEvaluationContext(target);
ctx.setVariable("target", target);
ctx.setVariable("self",
source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getSelf() : source);
ctx.setVariable(property.getName(), target);
return ctx; return ctx;
} }
@ -223,22 +243,30 @@ public final class ReferenceLookupDelegate {
* Compute the query to retrieve linked documents. * Compute the query to retrieve linked documents.
* *
* @param property must not be {@literal null}. * @param property must not be {@literal null}.
* @param value must not be {@literal null}. * @param source must not be {@literal null}.
* @param spELContext must not be {@literal null}. * @param spELContext must not be {@literal null}.
* @return never {@literal null}. * @return never {@literal null}.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) {
DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference() DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference()
: ReferenceEmulatingDocumentReference.INSTANCE; : ReferenceEmulatingDocumentReference.INSTANCE;
String lookup = documentReference.lookup(); String lookup = documentReference.lookup();
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), Object value = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
: source;
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, source, spELContext),
() -> new Document()); () -> new Document());
if (property.isCollectionLike() && value instanceof Collection) { if (property.isCollectionLike() && (value instanceof Collection || value == null)) {
if (value == null) {
return new ListDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, source, spELContext)),
sort);
}
List<Document> ors = new ArrayList<>(); List<Document> ors = new ArrayList<>();
for (Object entry : (Collection<Object>) value) { for (Object entry : (Collection<Object>) value) {
@ -263,7 +291,7 @@ public final class ReferenceLookupDelegate {
return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap); return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap);
} }
return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, source, spELContext)), sort);
} }
enum ReferenceEmulatingDocumentReference implements DocumentReference { enum ReferenceEmulatingDocumentReference implements DocumentReference {

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

@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils; import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils;
@ -1049,7 +1050,34 @@ public class MongoTemplateDocumentReferenceTests {
}); });
assertThat(target).containsEntry("publisher", "p-1"); assertThat(target).containsEntry("publisher", "p-1");
}
@Test // GH-3798
void allowsOneToMayStyleLookupsUsingSelfVariable() {
OneToManyStyleBook book1 = new OneToManyStyleBook();
book1.id = "id-1";
book1.publisherId = "p-100";
OneToManyStyleBook book2 = new OneToManyStyleBook();
book2.id = "id-2";
book2.publisherId = "p-200";
OneToManyStyleBook book3 = new OneToManyStyleBook();
book3.id = "id-3";
book3.publisherId = "p-100";
template.save(book1);
template.save(book2);
template.save(book3);
OneToManyStylePublisher publisher = new OneToManyStylePublisher();
publisher.id = "p-100";
template.save(publisher);
OneToManyStylePublisher target = template.findOne(query(where("id").is(publisher.id)), OneToManyStylePublisher.class);
assertThat(target.books).containsExactlyInAnyOrder(book1, book3);
} }
@Data @Data
@ -1293,4 +1321,24 @@ public class MongoTemplateDocumentReferenceTests {
@Reference // @Reference //
Publisher publisher; Publisher publisher;
} }
@Data
static class OneToManyStyleBook {
@Id
String id;
private String publisherId;
}
@Data
static class OneToManyStylePublisher {
@Id
String id;
@ReadOnlyProperty
@DocumentReference(lookup="{'publisherId':?#{#self._id} }")
List<OneToManyStyleBook> books;
}
} }

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

@ -262,6 +262,62 @@ class Publisher {
<2> The field value placeholders of the lookup query (like `acc`) is used to form the reference document. <2> The field value placeholders of the lookup query (like `acc`) is used to form the reference document.
==== ====
It is also possible to model relational style _One-To-Many_ references using a combination of `@ReadonlyProperty` and `@DocumentReference`.
This approach allows to link types without explicitly storing the linking values within the document itself as shown in the snipped below.
====
[source,java]
----
@Document
class Book {
@Id
ObjectId id;
String title;
List<String> author;
ObjectId publisherId; <1>
}
@Document
class Publisher {
@Id
ObjectId id;
String acronym;
String name;
@ReadOnlyProperty <2>
@DocumentReference(lookup="{'publisherId':?#{#self._id} }") <3>
List<Book> books;
}
----
.`Book` document
[source,json]
----
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisherId" : 8cfb002
}
----
.`Publisher` document
[source,json]
----
{
"_id" : 8cfb002,
"acronym" : "DR",
"name" : "Del Rey"
}
----
<1> Set up the link from `Book` to `Publisher` by storing the `Publisher.id` within the `Book` document.
<2> Mark the property holding the references to be read only. This prevents storing references to individual ``Book``s with the `Publisher` document.
<3> Use the `#self` variable to access values within the `Publisher` document and in this retrieve `Books` with matching `publisherId`.
====
With all the above in place it is possible to model all kind of associations between entities. With all the above in place it is possible to model all kind of associations between entities.
Have a look at the non-exhaustive list of samples below to get feeling for what is possible. Have a look at the non-exhaustive list of samples below to get feeling for what is possible.

Loading…
Cancel
Save