Skip to content

Allow OneToMany style lookups with via DocumentReference. #3802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-GH-3798-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-GH-3798-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-GH-3798-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-GH-3798-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@ private Object createLazyLoadingProxy(MongoPersistentProperty property, Object s
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
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;
Expand Down Expand Up @@ -524,28 +523,33 @@ private void readAssociation(Association<MongoPersistentProperty> association, P
MongoPersistentProperty property = association.getInverse();
Object value = documentAccessor.get(property);

if (value == null) {
return;
}

if (property.isDocumentReference()
|| (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) {

// quite unusual but sounds like worth having?

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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,20 @@ public 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);
Expand Down Expand Up @@ -196,8 +199,16 @@ private <T> T parseValueOrGet(String value, ParameterBindingContext bindingConte

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) {
Expand All @@ -212,9 +223,18 @@ ValueProvider valueProviderFor(Object source) {

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;
}
Expand All @@ -223,22 +243,30 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object
* 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) {
Expand All @@ -263,7 +291,7 @@ DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object va
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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;
Expand Down Expand Up @@ -1049,7 +1050,34 @@ void updateWhenUsingAtReferenceDirectly() {
});

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
Expand Down Expand Up @@ -1293,4 +1321,24 @@ static class UsingAtReference {
@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 changes: 56 additions & 0 deletions src/main/asciidoc/reference/document-references.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down