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 { @@ -108,6 +108,6 @@ public class DefaultReferenceResolver implements ReferenceResolver {
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
return proxyFactory.createLazyLoadingProxy(property, it -> {
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 @@ @@ -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; @@ -38,7 +38,6 @@ import org.bson.json.JsonReader;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@ -524,10 +523,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -524,10 +523,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
MongoPersistentProperty property = association.getInverse();
Object value = documentAccessor.get(property);
if (value == null) {
return;
}
if (property.isDocumentReference()
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) {
@ -535,14 +530,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -535,14 +530,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) {
if(value == null) {
return;
}
DocumentPointer<?> pointer = () -> value;
// collection like special treatment
accessor.setProperty(property, conversionService.convert(pointer, property.getActualType()));
} else {
accessor.setProperty(property,
dbRefResolver.resolveReference(property, value, referenceLookupDelegate, context::convert));
dbRefResolver.resolveReference(property, new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)), referenceLookupDelegate, context::convert));
}
return;
}
if (value == null) {
return;
}

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

@ -87,17 +87,20 @@ public final class ReferenceLookupDelegate { @@ -87,17 +87,20 @@ public final class ReferenceLookupDelegate {
* Read the reference expressed by the given property.
*
* @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 entityReader the callback to convert raw source values into actual domain types. Must not be
* {@literal null}.
* @return can be {@literal null}.
*/
@Nullable
public Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction,
public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
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);
Iterable<Document> result = lookupFunction.apply(filter, referenceCollection);
@ -196,8 +199,16 @@ public final class ReferenceLookupDelegate { @@ -196,8 +199,16 @@ public final class ReferenceLookupDelegate {
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));
}
ValueProvider valueProviderFor(Object source) {
@ -212,9 +223,18 @@ public final class ReferenceLookupDelegate { @@ -212,9 +223,18 @@ public final class ReferenceLookupDelegate {
EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
EvaluationContext ctx = spELContext.getEvaluationContext(source);
ctx.setVariable("target", source);
ctx.setVariable(property.getName(), source);
Object target = source instanceof DocumentReferenceSource ? ((DocumentReferenceSource) source).getTargetSource()
: 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;
}
@ -223,22 +243,30 @@ public final class ReferenceLookupDelegate { @@ -223,22 +243,30 @@ public final class ReferenceLookupDelegate {
* Compute the query to retrieve linked documents.
*
* @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}.
* @return never {@literal null}.
*/
@SuppressWarnings("unchecked")
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) {
DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference()
: ReferenceEmulatingDocumentReference.INSTANCE;
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());
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<>();
for (Object entry : (Collection<Object>) value) {
@ -263,7 +291,7 @@ public final class ReferenceLookupDelegate { @@ -263,7 +291,7 @@ public final class ReferenceLookupDelegate {
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 {

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

@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test; @@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils;
@ -1049,7 +1050,34 @@ public class MongoTemplateDocumentReferenceTests { @@ -1049,7 +1050,34 @@ public class MongoTemplateDocumentReferenceTests {
});
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
@ -1293,4 +1321,24 @@ public class MongoTemplateDocumentReferenceTests { @@ -1293,4 +1321,24 @@ public class MongoTemplateDocumentReferenceTests {
@Reference //
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 { @@ -262,6 +262,62 @@ class Publisher {
<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.
Have a look at the non-exhaustive list of samples below to get feeling for what is possible.

Loading…
Cancel
Save