From 040a7eeff793a378496f3a0d57eb24f0c5cadf77 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 7 Sep 2021 08:55:05 +0200 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 5d28c8a5c5..dd821fe8f3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3798-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 0033bd11d5..2a74d848ca 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3798-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index f62c8dc7f4..bccb6a40da 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3798-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 1f157e75bc..e9f92255a9 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3798-SNAPSHOT ../pom.xml From 7f992407c28790e345d8fef8b7908bffe0a94936 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 7 Sep 2021 11:07:27 +0200 Subject: [PATCH 2/2] Allow OneToMany 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 books; } --- .../convert/DefaultReferenceResolver.java | 2 +- .../core/convert/DocumentReferenceSource.java | 63 +++++++++++++++++++ .../core/convert/MappingMongoConverter.java | 16 +++-- .../core/convert/ReferenceLookupDelegate.java | 52 +++++++++++---- .../MongoTemplateDocumentReferenceTests.java | 48 ++++++++++++++ .../reference/document-references.adoc | 56 +++++++++++++++++ 6 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java index f801b8d990..62e713065f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java @@ -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); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java new file mode 100644 index 0000000000..03e5eb0d5d --- /dev/null +++ b/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; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index a60c853c33..5a2c3e952a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -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; @@ -524,10 +523,6 @@ private void readAssociation(Association 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)) { @@ -535,17 +530,26 @@ private void readAssociation(Association association, P 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)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index 3ca730452f..e16f9024b5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java @@ -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 result = lookupFunction.apply(filter, referenceCollection); @@ -196,8 +199,16 @@ private 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) { @@ -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; } @@ -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 ors = new ArrayList<>(); for (Object entry : (Collection) value) { @@ -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 { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java index d6bcc10e49..06d288d1f5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java @@ -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; @@ -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 @@ -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 books; + } } diff --git a/src/main/asciidoc/reference/document-references.adoc b/src/main/asciidoc/reference/document-references.adoc index 885d2d6ade..23bc025e80 100644 --- a/src/main/asciidoc/reference/document-references.adoc +++ b/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. ==== +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 author; + + ObjectId publisherId; <1> +} + +@Document +class Publisher { + + @Id + ObjectId id; + String acronym; + String name; + + @ReadOnlyProperty <2> + @DocumentReference(lookup="{'publisherId':?#{#self._id} }") <3> + List 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.