diff --git a/pom.xml b/pom.xml index 0ff20c5825..1fedf3a50d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..07c8902539 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 - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 47a5b7aba7..6843174685 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT ../pom.xml @@ -50,7 +50,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..0585cfd5d4 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b86dc2808c..cb9a6f68de 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-2004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index 5db38cfc96..701d7497fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -58,6 +58,14 @@ public DocumentAccessor(Bson document) { this.document = document; } + /** + * @return the underlying {@link Bson document}. + * @since 2.1 + */ + public Bson getDocument() { + return this.document; + } + /** * Puts the given value into the backing {@link Document} based on the coordinates defined through the given * {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist 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 5c9410af6c..a90a0dd0ce 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 @@ -255,7 +255,8 @@ private S read(TypeInformation type, @Nullable Bson bson, private ParameterValueProvider getParameterProvider(MongoPersistentEntity entity, Bson source, DefaultSpELExpressionEvaluator evaluator, ObjectPath path) { - MongoDbPropertyValueProvider provider = new MongoDbPropertyValueProvider(source, evaluator, path); + AssociationAwareMongoDbPropertyValueProvider provider = new AssociationAwareMongoDbPropertyValueProvider(source, + evaluator, path); PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( entity, provider, path.getCurrentObject()); @@ -659,7 +660,8 @@ protected Bson createMap(Map map, MongoPersistentProperty proper * @param sink the {@link Collection} to write to. * @return */ - private List writeCollectionInternal(Collection source, @Nullable TypeInformation type, Collection sink) { + private List writeCollectionInternal(Collection source, @Nullable TypeInformation type, + Collection sink) { TypeInformation componentType = null; @@ -867,7 +869,7 @@ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) { */ @Nullable @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { return value; @@ -1270,12 +1272,14 @@ private Object removeTypeInfo(Object object, boolean recursively) { * of the configured source {@link Document}. * * @author Oliver Gierke + * @author Mark Paluch + * @author Christoph Strobl */ class MongoDbPropertyValueProvider implements PropertyValueProvider { - private final DocumentAccessor source; - private final SpELExpressionEvaluator evaluator; - private final ObjectPath path; + final DocumentAccessor source; + final SpELExpressionEvaluator evaluator; + final ObjectPath path; /** * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and @@ -1285,15 +1289,8 @@ class MongoDbPropertyValueProvider implements PropertyValueProvider T getPropertyValue(MongoPersistentProperty property) { String expression = property.getSpelExpression(); @@ -1333,6 +1331,54 @@ public T getPropertyValue(MongoPersistentProperty property) { } } + /** + * {@link PropertyValueProvider} that is aware of {@link MongoPersistentProperty#isAssociation()} and that delegates + * resolution to {@link DbRefResolver}. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ + class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueProvider { + + /** + * Creates a new {@link AssociationAwareMongoDbPropertyValueProvider} for the given source, + * {@link SpELExpressionEvaluator} and {@link ObjectPath}. + * + * @param source must not be {@literal null}. + * @param evaluator must not be {@literal null}. + * @param path must not be {@literal null}. + */ + AssociationAwareMongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath path) { + super(source, evaluator, path); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) + */ + @Nullable + @SuppressWarnings("unchecked") + public T getPropertyValue(MongoPersistentProperty property) { + + if (property.isDbReference() && property.getDBRef().lazy()) { + + Object rawRefValue = source.get(property); + if (rawRefValue == null) { + return null; + } + + DbRefResolverCallback callback = new DefaultDbRefResolverCallback(source.getDocument(), path, evaluator, + MappingMongoConverter.this); + + DBRef dbref = rawRefValue instanceof DBRef ? (DBRef) rawRefValue : null; + return (T) dbRefResolver.resolveDbRef(property, dbref, callback, dbRefProxyHandler); + } + + return super.getPropertyValue(property); + } + } + /** * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw * resolved SpEL value. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 26193ad483..144c5c48bd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -3069,8 +3069,8 @@ public void resolvesCyclicDBRefCorrectly() { assertThat(contentLoaded.dbrefMessage.id, is(messageLoaded.id)); } - @Test // DATAMONGO-1287 - public void shouldReuseAlreadyResolvedLazyLoadedDBRefWhenUsedAsPersistenceConstrcutorArgument() { + @Test // DATAMONGO-1287, DATAMONGO-2004 + public void shouldReuseAlreadyResolvedLazyLoadedDBRefWhenUsedAsPersistenceConstructorArgument() { Document docInCtor = new Document(); docInCtor.id = "doc-in-ctor"; @@ -3083,7 +3083,7 @@ public void shouldReuseAlreadyResolvedLazyLoadedDBRefWhenUsedAsPersistenceConstr DocumentWithLazyDBrefUsedInPresistenceConstructor loaded = template.findOne(query(where("id").is(source.id)), DocumentWithLazyDBrefUsedInPresistenceConstructor.class); - assertThat(loaded.refToDocUsedInCtor, not(instanceOf(LazyLoadingProxy.class))); + assertThat(loaded.refToDocUsedInCtor, instanceOf(LazyLoadingProxy.class)); assertThat(loaded.refToDocNotUsedInCtor, nullValue()); } @@ -3106,8 +3106,8 @@ public void shouldNotReuseLazyLoadedDBRefWhenTypeUsedInPersistenceConstrcutorBut assertThat(loaded.refToDocUsedInCtor, nullValue()); } - @Test // DATAMONGO-1287 - public void shouldRespectParamterValueWhenAttemptingToReuseLazyLoadedDBRefUsedInPersistenceConstrcutor() { + @Test // DATAMONGO-1287, DATAMONGO-2004 + public void shouldRespectParameterValueWhenAttemptingToReuseLazyLoadedDBRefUsedInPersistenceConstructor() { Document docInCtor = new Document(); docInCtor.id = "doc-in-ctor"; @@ -3125,7 +3125,7 @@ public void shouldRespectParamterValueWhenAttemptingToReuseLazyLoadedDBRefUsedIn DocumentWithLazyDBrefUsedInPresistenceConstructor loaded = template.findOne(query(where("id").is(source.id)), DocumentWithLazyDBrefUsedInPresistenceConstructor.class); - assertThat(loaded.refToDocUsedInCtor, not(instanceOf(LazyLoadingProxy.class))); + assertThat(loaded.refToDocUsedInCtor, instanceOf(LazyLoadingProxy.class)); assertThat(loaded.refToDocNotUsedInCtor, instanceOf(LazyLoadingProxy.class)); } @@ -3384,6 +3384,73 @@ public void shouldFetchMapOfLazyReferencesCorrectly() { assertThat(target.lazyDbRefAnnotatedMap.values(), contains(two, one)); } + @Test // DATAMONGO-2004 + public void shouldFetchLazyReferenceWithConstructorCreationCorrectly() { + + Sample one = new Sample("1", "jon snow"); + + template.save(one); + + DocumentWithLazyDBRefsAndConstructorCreation source = new DocumentWithLazyDBRefsAndConstructorCreation(null, one, + null, null); + + template.save(source); + + DocumentWithLazyDBRefsAndConstructorCreation target = template.findOne(query(where("id").is(source.id)), + DocumentWithLazyDBRefsAndConstructorCreation.class); + + assertThat(target.lazyDbRefProperty, instanceOf(LazyLoadingProxy.class)); + assertThat(target.lazyDbRefProperty, is(one)); + } + + @Test // DATAMONGO-2004 + public void shouldFetchMapOfLazyReferencesWithConstructorCreationCorrectly() { + + Sample one = new Sample("1", "jon snow"); + Sample two = new Sample("2", "tyrion lannister"); + + template.save(one); + template.save(two); + + Map map = new LinkedHashMap<>(); + map.put("tyrion", two); + map.put("jon", one); + + DocumentWithLazyDBRefsAndConstructorCreation source = new DocumentWithLazyDBRefsAndConstructorCreation(null, null, + null, map); + + template.save(source); + + DocumentWithLazyDBRefsAndConstructorCreation target = template.findOne(query(where("id").is(source.id)), + DocumentWithLazyDBRefsAndConstructorCreation.class); + + assertThat(target.lazyDbRefAnnotatedMap, instanceOf(LazyLoadingProxy.class)); + assertThat(target.lazyDbRefAnnotatedMap.values(), contains(two, one)); + } + + @Test // DATAMONGO-2004 + public void shouldFetchListOfLazyReferencesWithConstructorCreationCorrectly() { + + Sample one = new Sample("1", "jon snow"); + Sample two = new Sample("2", "tyrion lannister"); + + template.save(one); + template.save(two); + + List list = Arrays.asList(two, one); + + DocumentWithLazyDBRefsAndConstructorCreation source = new DocumentWithLazyDBRefsAndConstructorCreation(null, null, + list, null); + + template.save(source); + + DocumentWithLazyDBRefsAndConstructorCreation target = template.findOne(query(where("id").is(source.id)), + DocumentWithLazyDBRefsAndConstructorCreation.class); + + assertThat(target.lazyDbRefAnnotatedList, instanceOf(LazyLoadingProxy.class)); + assertThat(target.getLazyDbRefAnnotatedList(), contains(two, one)); + } + @Test // DATAMONGO-1513 @DirtiesContext public void populatesIdsAddedByEventListener() { @@ -3590,6 +3657,29 @@ static class DocumentWithDBRefCollection { @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) public Map lazyDbRefAnnotatedMap; } + @Data + static class DocumentWithLazyDBRefsAndConstructorCreation { + + @Id public final String id; + + public DocumentWithLazyDBRefsAndConstructorCreation(String id, Sample lazyDbRefProperty, + List lazyDbRefAnnotatedList, Map lazyDbRefAnnotatedMap) { + this.id = id; + this.lazyDbRefProperty = lazyDbRefProperty; + this.lazyDbRefAnnotatedList = lazyDbRefAnnotatedList; + this.lazyDbRefAnnotatedMap = lazyDbRefAnnotatedMap; + } + + @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) // + public final Sample lazyDbRefProperty; + + @Field("lazy_db_ref_list") @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) // + public final List lazyDbRefAnnotatedList; + + @Field("lazy_db_ref_map") @org.springframework.data.mongodb.core.mapping.DBRef( + lazy = true) public final Map lazyDbRefAnnotatedMap; + } + @EqualsAndHashCode static class DocumentWithCollection { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterTests.java new file mode 100644 index 0000000000..454d468c2e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2018 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 + * + * http://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 static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.List; + +import org.bson.Document; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.MongoClient; + +/** + * Integration tests for {@link MappingMongoConverter}. + * + * @author Christoph Strobl + */ +public class MappingMongoConverterTests { + + MongoClient client; + + MappingMongoConverter converter; + MongoMappingContext mappingContext; + DbRefResolver dbRefResolver; + + @Before + public void setUp() { + + client = new MongoClient(); + client.dropDatabase("mapping-converter-tests"); + + MongoDbFactory factory = new SimpleMongoDbFactory(client, "mapping-converter-tests"); + + dbRefResolver = spy(new DefaultDbRefResolver(factory)); + mappingContext = new MongoMappingContext(); + mappingContext.afterPropertiesSet(); + + converter = new MappingMongoConverter(dbRefResolver, mappingContext); + } + + @Test // DATAMONGO-2004 + public void resolvesLazyDBRefOnAccess() { + + client.getDatabase("mapping-converter-tests").getCollection("samples") + .insertMany(Arrays.asList(new Document("_id", "sample-1").append("value", "one"), + new Document("_id", "sample-2").append("value", "two"))); + + Document source = new Document("_id", "id-1").append("lazyList", + Arrays.asList(new com.mongodb.DBRef("samples", "sample-1"), new com.mongodb.DBRef("samples", "sample-2"))); + + WithLazyDBRef target = converter.read(WithLazyDBRef.class, source); + + verify(dbRefResolver).resolveDbRef(any(), isNull(), any(), any()); + verifyNoMoreInteractions(dbRefResolver); + + assertThat(target.lazyList).isInstanceOf(LazyLoadingProxy.class); + assertThat(target.getLazyList()).contains(new Sample("sample-1", "one"), new Sample("sample-2", "two")); + + verify(dbRefResolver).bulkFetch(any()); + } + + @Test // DATAMONGO-2004 + public void resolvesLazyDBRefConstructorArgOnAccess() { + + client.getDatabase("mapping-converter-tests").getCollection("samples") + .insertMany(Arrays.asList(new Document("_id", "sample-1").append("value", "one"), + new Document("_id", "sample-2").append("value", "two"))); + + Document source = new Document("_id", "id-1").append("lazyList", + Arrays.asList(new com.mongodb.DBRef("samples", "sample-1"), new com.mongodb.DBRef("samples", "sample-2"))); + + WithLazyDBRefAsConstructorArg target = converter.read(WithLazyDBRefAsConstructorArg.class, source); + + verify(dbRefResolver).resolveDbRef(any(), isNull(), any(), any()); + verifyNoMoreInteractions(dbRefResolver); + + assertThat(target.lazyList).isInstanceOf(LazyLoadingProxy.class); + assertThat(target.getLazyList()).contains(new Sample("sample-1", "one"), new Sample("sample-2", "two")); + + verify(dbRefResolver).bulkFetch(any()); + } + + public static class WithLazyDBRef { + + @Id String id; + @DBRef(lazy = true) List lazyList; + + public List getLazyList() { + return lazyList; + } + } + + public static class WithLazyDBRefAsConstructorArg { + + @Id String id; + @DBRef(lazy = true) List lazyList; + + public WithLazyDBRefAsConstructorArg(String id, List lazyList) { + + this.id = id; + this.lazyList = lazyList; + } + + public List getLazyList() { + return lazyList; + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Sample { + + @Id String id; + String value; + } +} diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 69beb09220..2f863ebcd2 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -584,9 +584,13 @@ public class Person { ==== You need not use `@OneToMany` or similar mechanisms because the List of objects tells the mapping framework that you want a one-to-many relationship. When the object is stored in MongoDB, there is a list of DBRefs rather than the `Account` objects themselves. +When it comes to loading collections of ``DBRef``s it is advisable to restrict references held in collection types to a specific MongoDB collection. This allows bulk loading of all references, whereas references pointing to different MongoDB collections need to be resolved one by one. IMPORTANT: The mapping framework does not handle cascading saves. If you change an `Account` object that is referenced by a `Person` object, you must save the `Account` object separately. Calling `save` on the `Person` object does not automatically save the `Account` objects in the `accounts` property. +``DBRef``s can also be resolved lazily. In this case the actual `Object` or `Collection` of references is resolved on first access of the property. Use the `lazy` attribute of `@DBRef` to specify this. +Required properties that are also defined as lazy loading ``DBRef`` and used as constructor arguments are also decorated with the lazy loading proxy making sure to put as little pressure on the database and network as possible. + [[mapping-usage-events]] === Mapping Framework Events