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.
This commit is contained in:
committed by
Mark Paluch
parent
dcf184888e
commit
c8307d5a39
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
MongoPersistentProperty property = association.getInverse();
|
||||
Object value = documentAccessor.get(property);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.isDocumentReference()
|
||||
|| (!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(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;
|
||||
}
|
||||
|
||||
DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
|
||||
|
||||
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
* 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 {
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
|
||||
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 {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user