{
+
+ /**
+ * The actual pointer value. This can be any simple type, like a {@link String} or {@link org.bson.types.ObjectId} or
+ * a {@link org.bson.Document} holding more information like the target collection, multiple fields forming the key,
+ * etc.
+ *
+ * @return the value stored in MongoDB and used for constructing the {@link DocumentReference#lookup() lookup query}.
+ */
+ T getPointer();
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java
new file mode 100644
index 0000000000..0846c4022c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java
@@ -0,0 +1,132 @@
+/*
+ * 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.mapping;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.data.annotation.Reference;
+
+/**
+ * A {@link DocumentReference} offers an alternative way of linking entities in MongoDB. While the goal is the same as
+ * when using {@link DBRef}, the store representation is different and can be literally anything, a single value, an
+ * entire {@link org.bson.Document}, basically everything that can be stored in MongoDB. By default, the mapping layer
+ * will use the referenced entities {@literal id} value for storage and retrieval.
+ *
+ *
+ * public class Account {
+ * private String id;
+ * private Float total;
+ * }
+ *
+ * public class Person {
+ * private String id;
+ * @DocumentReference
+ * private List<Account> accounts;
+ * }
+ *
+ * Account account = ...
+ *
+ * mongoTemplate.insert(account);
+ *
+ * template.update(Person.class)
+ * .matching(where("id").is(...))
+ * .apply(new Update().push("accounts").value(account))
+ * .first();
+ *
+ *
+ * {@link #lookup()} allows to define custom queries that are independent from the {@literal id} field and in
+ * combination with {@link org.springframework.data.convert.WritingConverter writing converters} offer a flexible way of
+ * defining links between entities.
+ *
+ *
+ * public class Book {
+ * private ObjectId id;
+ * private String title;
+ *
+ * @Field("publisher_ac")
+ * @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }")
+ * private Publisher publisher;
+ * }
+ *
+ * public class Publisher {
+ *
+ * private ObjectId id;
+ * private String acronym;
+ * private String name;
+ *
+ * @DocumentReference(lazy = true)
+ * private List<Book> books;
+ * }
+ *
+ * @WritingConverter
+ * public class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {
+ *
+ * public DocumentPointer<String> convert(Publisher source) {
+ * return () -> source.getAcronym();
+ * }
+ * }
+ *
+ *
+ * @author Christoph Strobl
+ * @since 3.3
+ * @see MongoDB Reference Documentation
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD })
+@Reference
+public @interface DocumentReference {
+
+ /**
+ * The database the linked entity resides in.
+ *
+ * @return empty String by default. Uses the default database provided buy the {@link org.springframework.data.mongodb.MongoDatabaseFactory}.
+ */
+ String db() default "";
+
+ /**
+ * The database the linked entity resides in.
+ *
+ * @return empty String by default. Uses the property type for collection resolution.
+ */
+ String collection() default "";
+
+ /**
+ * The single document lookup query. In case of an {@link java.util.Collection} or {@link java.util.Map} property
+ * the individual lookups are combined via an `$or` operator.
+ *
+ * @return an {@literal _id} based lookup.
+ */
+ String lookup() default "{ '_id' : ?#{#target} }";
+
+ /**
+ * A specific sort.
+ *
+ * @return empty String by default.
+ */
+ String sort() default "";
+
+ /**
+ * Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}.
+ *
+ * @return {@literal false} by default.
+ */
+ boolean lazy() default false;
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
index 7c347229b6..c753f3856d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
@@ -62,6 +62,15 @@ public interface MongoPersistentProperty extends PersistentProperty {
+
+ cfg.configureDatabaseFactory(it -> {
+
+ it.client(client);
+ it.defaultDb(DB_NAME);
+ });
+
+ cfg.configureConversion(it -> {
+ it.customConverters(new ReferencableConverter(), new SimpleObjectRefWithReadingConverterToDocumentConverter(),
+ new DocumentToSimpleObjectRefWithReadingConverter());
+ });
+
+ cfg.configureMappingContext(it -> {
+ it.autocreateIndex(false);
+ });
+ });
+
+ @BeforeEach
+ public void setUp() {
+ template.flushDatabase();
+ }
+
+ @Test // GH-3602
+ void writeSimpleTypeReference() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+
+ SingleRefRoot source = new SingleRefRoot();
+ source.id = "root-1";
+ source.simpleValueRef = new SimpleObjectRef("ref-1", "me-the-referenced-object");
+
+ template.save(source);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
+ }
+
+ @Test // GH-3602
+ void writeMapTypeReference() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot source = new CollectionRefRoot();
+ source.id = "root-1";
+ source.mapValueRef = new LinkedHashMap<>();
+ source.mapValueRef.put("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"));
+ source.mapValueRef.put("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
+
+ template.save(source);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2");
+ }
+
+ @Test // GH-3602
+ void writeCollectionOfSimpleTypeReference() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot source = new CollectionRefRoot();
+ source.id = "root-1";
+ source.simpleValueRef = Arrays.asList(new SimpleObjectRef("ref-1", "me-the-1-referenced-object"),
+ new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
+
+ template.save(source);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
+ }
+
+ @Test // GH-3602
+ void writeObjectTypeReference() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+
+ SingleRefRoot source = new SingleRefRoot();
+ source.id = "root-1";
+ source.objectValueRef = new ObjectRefOfDocument("ref-1", "me-the-referenced-object");
+
+ template.save(source);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
+ }
+
+ @Test // GH-3602
+ void writeCollectionOfObjectTypeReference() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot source = new CollectionRefRoot();
+ source.id = "root-1";
+ source.objectValueRef = Arrays.asList(new ObjectRefOfDocument("ref-1", "me-the-1-referenced-object"),
+ new ObjectRefOfDocument("ref-2", "me-the-2-referenced-object"));
+
+ template.save(source);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target.get("objectValueRef", List.class)).containsExactly(
+ source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
+ }
+
+ @Test // GH-3602
+ void readSimpleTypeObjectReference() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef", "ref-1");
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+ assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readCollectionOfSimpleTypeObjectReference() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
+ Collections.singletonList("ref-1"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readLazySimpleTypeObjectReference() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simpleLazyValueRef", "ref-1");
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+
+ LazyLoadingTestUtils.assertProxy(result.simpleLazyValueRef, (proxy) -> {
+
+ assertThat(proxy.isResolved()).isFalse();
+ assertThat(proxy.currentValue()).isNull();
+ });
+ assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
+ "ref-1");
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+ assertThat(result.getSimpleValueRefWithAnnotatedFieldName())
+ .isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
+ Collections.singletonList("ref-1"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getSimpleValueRefWithAnnotatedFieldName())
+ .containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readObjectReferenceFromDocumentType() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
+ new Document("id", "ref-1").append("property", "without-any-meaning"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+ assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readCollectionObjectReferenceFromDocumentType() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
+ Collections.singletonList(new Document("id", "ref-1").append("property", "without-any-meaning")));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getObjectValueRef())
+ .containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readObjectReferenceFromDocumentDeclaringCollectionName() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
+ Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append(
+ "objectValueRefWithEmbeddedCollectionName",
+ new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
+ .append("property", "without-any-meaning"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+ assertThat(result.getObjectValueRefWithEmbeddedCollectionName())
+ .isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
+ Document refSource1 = new Document("_id", "ref-1").append("value", "me-the-1-referenced-object");
+ Document refSource2 = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append(
+ "objectValueRefWithEmbeddedCollectionName",
+ Arrays.asList(
+ new Document("id", "ref-2").append("collection", "object-ref-of-document-with-embedded-collection-name"),
+ new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
+ .append("property", "without-any-meaning")));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource1);
+ db.getCollection(refCollectionName).insertOne(refSource2);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getObjectValueRefWithEmbeddedCollectionName()).containsExactly(
+ new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-2", "me-the-2-referenced-object"),
+ new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void useOrderFromAnnotatedSort() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+ Document refSource1 = new Document("_id", "ref-1").append("value", "me-the-1-referenced-object");
+ Document refSource2 = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
+ Document refSource3 = new Document("_id", "ref-3").append("value", "me-the-3-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simpleSortedValueRef",
+ Arrays.asList("ref-1", "ref-3", "ref-2"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource1);
+ db.getCollection(refCollectionName).insertOne(refSource2);
+ db.getCollection(refCollectionName).insertOne(refSource3);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getSimpleSortedValueRef()).containsExactly(
+ new SimpleObjectRef("ref-3", "me-the-3-referenced-object"),
+ new SimpleObjectRef("ref-2", "me-the-2-referenced-object"),
+ new SimpleObjectRef("ref-1", "me-the-1-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
+ Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
+ .append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
+ new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+ assertThat(result.getObjectValueRefOnNonIdFields())
+ .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
+ }
+
+ @Test // GH-3602
+ void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
+
+ String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
+ String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
+ Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
+ .append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("lazyObjectValueRefOnNonIdFields",
+ new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
+
+ LazyLoadingTestUtils.assertProxy(result.lazyObjectValueRefOnNonIdFields, (proxy) -> {
+
+ assertThat(proxy.isResolved()).isFalse();
+ assertThat(proxy.currentValue()).isNull();
+ });
+ assertThat(result.getLazyObjectValueRefOnNonIdFields())
+ .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
+ }
+
+ @Test // GH-3602
+ void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
+ Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
+ .append("value", "me-the-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
+ Collections.singletonList(new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property",
+ "without-any-meaning")));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getObjectValueRefOnNonIdFields())
+ .containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
+ }
+
+ @Test // GH-3602
+ void readMapOfReferences() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+
+ Document refSource1 = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
+ .append("value", "me-the-1-referenced-object");
+
+ Document refSource2 = new Document("_id", "ref-2").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
+ .append("value", "me-the-2-referenced-object");
+
+ Map refmap = new LinkedHashMap<>();
+ refmap.put("frodo", "ref-1");
+ refmap.put("bilbo", "ref-2");
+
+ Document source = new Document("_id", "id-1").append("value", "v1").append("mapValueRef", refmap);
+
+ template.execute(db -> {
+
+ db.getCollection(rootCollectionName).insertOne(source);
+ db.getCollection(refCollectionName).insertOne(refSource1);
+ db.getCollection(refCollectionName).insertOne(refSource2);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+
+ assertThat(result.getMapValueRef())
+ .containsEntry("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
+ .containsEntry("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void loadLazyCyclicReference() {
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+
+ a.toB = b;
+ b.lazyToA = a;
+
+ template.save(a);
+ template.save(b);
+
+ WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
+ assertThat(loadedA).isNotNull();
+ assertThat(loadedA.getToB()).isNotNull();
+ LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
+
+ assertThat(proxy.isResolved()).isFalse();
+ assertThat(proxy.currentValue()).isNull();
+ });
+ }
+
+ @Test // GH-3602
+ void loadEagerCyclicReference() {
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+
+ a.toB = b;
+ b.eagerToA = a;
+
+ template.save(a);
+ template.save(b);
+
+ WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
+
+ assertThat(loadedA).isNotNull();
+ assertThat(loadedA.getToB()).isNotNull();
+ assertThat(loadedA.getToB().eagerToA).isSameAs(loadedA);
+ }
+
+ @Test // GH-3602
+ void loadAndStoreUnresolvedLazyDoesNotResolveTheProxy() {
+
+ String collectionB = template.getCollectionName(WithRefB.class);
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+
+ a.toB = b;
+ b.lazyToA = a;
+
+ template.save(a);
+ template.save(b);
+
+ WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
+ template.save(loadedA.getToB());
+
+ LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
+
+ assertThat(proxy.isResolved()).isFalse();
+ assertThat(proxy.currentValue()).isNull();
+ });
+
+ Document target = template.execute(db -> {
+ return db.getCollection(collectionB).find(Filters.eq("_id", "b")).first();
+ });
+ assertThat(target.get("lazyToA", Object.class)).isEqualTo("a");
+ }
+
+ @Test // GH-3602
+ void loadCollectionReferenceWithMissingRefs() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+ String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
+
+ // ref-1 is missing.
+ Document refSource = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
+ Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
+ Arrays.asList("ref-1", "ref-2"));
+
+ template.execute(db -> {
+
+ db.getCollection(refCollectionName).insertOne(refSource);
+ db.getCollection(rootCollectionName).insertOne(source);
+ return null;
+ });
+
+ CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
+ assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
+ }
+
+ @Test // GH-3602
+ void queryForReference() {
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+ template.save(b);
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+ a.toB = b;
+ template.save(a);
+
+ WithRefA a2 = new WithRefA();
+ a2.id = "a2";
+ template.save(a2);
+
+ WithRefA loadedA = template.query(WithRefA.class).matching(where("toB").is(b)).firstValue();
+ assertThat(loadedA.getId()).isEqualTo(a.getId());
+ }
+
+ @Test // GH-3602
+ void queryForReferenceInCollection() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ Document shouldBeFound = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
+ Arrays.asList("ref-1", "ref-2"));
+ Document shouldNotBeFound = new Document("_id", "id-2").append("value", "v2").append("simpleValueRef",
+ Arrays.asList("ref-1"));
+
+ template.execute(db -> {
+
+ db.getCollection(rootCollectionName).insertOne(shouldBeFound);
+ db.getCollection(rootCollectionName).insertOne(shouldNotBeFound);
+ return null;
+ });
+
+ SimpleObjectRef objectRef = new SimpleObjectRef("ref-2", "some irrelevant value");
+
+ List loaded = template.query(CollectionRefRoot.class)
+ .matching(where("simpleValueRef").in(objectRef)).all();
+ assertThat(loaded).map(CollectionRefRoot::getId).containsExactly("id-1");
+ }
+
+ @Test // GH-3602
+ void queryForReferenceOnIdField() {
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+ template.save(b);
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+ a.toB = b;
+ template.save(a);
+
+ WithRefA a2 = new WithRefA();
+ a2.id = "a2";
+ template.save(a2);
+
+ WithRefA loadedA = template.query(WithRefA.class).matching(where("toB.id").is(b.id)).firstValue();
+ assertThat(loadedA.getId()).isEqualTo(a.getId());
+ }
+
+ @Test // GH-3602
+ void updateReferenceWithEntityHavingPointerConversion() {
+
+ WithRefB b = new WithRefB();
+ b.id = "b";
+ template.save(b);
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+ template.save(a);
+
+ template.update(WithRefA.class).apply(new Update().set("toB", b)).first();
+
+ String collectionA = template.getCollectionName(WithRefA.class);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(collectionA).find(Filters.eq("_id", "a")).first();
+ });
+
+ assertThat(target).containsEntry("toB", "b");
+ }
+
+ @Test // GH-3602
+ void updateReferenceWithEntityWithoutPointerConversion() {
+
+ String collectionName = template.getCollectionName(SingleRefRoot.class);
+ SingleRefRoot refRoot = new SingleRefRoot();
+ refRoot.id = "root-1";
+
+ SimpleObjectRef ref = new SimpleObjectRef("ref-1", "me the referenced object");
+
+ template.save(refRoot);
+
+ template.update(SingleRefRoot.class).apply(new Update().set("simpleValueRef", ref)).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(collectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target).containsEntry("simpleValueRef", "ref-1");
+ }
+
+ @Test // GH-3602
+ void updateReferenceWithValue() {
+
+ WithRefA a = new WithRefA();
+ a.id = "a";
+ template.save(a);
+
+ template.update(WithRefA.class).apply(new Update().set("toB", "b")).first();
+
+ String collectionA = template.getCollectionName(WithRefA.class);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(collectionA).find(Filters.eq("_id", "a")).first();
+ });
+
+ assertThat(target).containsEntry("toB", "b");
+ }
+
+ @Test // GH-3602
+ void updateReferenceCollectionWithEntity() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot root = new CollectionRefRoot();
+ root.id = "root-1";
+ root.simpleValueRef = Collections.singletonList(new SimpleObjectRef("ref-1", "beastie"));
+
+ template.save(root);
+
+ template.update(CollectionRefRoot.class)
+ .apply(new Update().push("simpleValueRef").value(new SimpleObjectRef("ref-2", "boys"))).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target).containsEntry("simpleValueRef", Arrays.asList("ref-1", "ref-2"));
+ }
+
+ @Test // GH-3602
+ void updateReferenceCollectionWithValue() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot root = new CollectionRefRoot();
+ root.id = "root-1";
+ root.simpleValueRef = Collections.singletonList(new SimpleObjectRef("ref-1", "beastie"));
+
+ template.save(root);
+
+ template.update(CollectionRefRoot.class).apply(new Update().push("simpleValueRef").value("ref-2")).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target).containsEntry("simpleValueRef", Arrays.asList("ref-1", "ref-2"));
+ }
+
+ @Test // GH-3602
+ @Disabled("Property path resolution does not work inside maps, the key is considered :/")
+ void updateReferenceMapWithEntity() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot root = new CollectionRefRoot();
+ root.id = "root-1";
+ root.mapValueRef = Collections.singletonMap("beastie", new SimpleObjectRef("ref-1", "boys"));
+
+ template.save(root);
+
+ template.update(CollectionRefRoot.class)
+ .apply(new Update().set("mapValueRef.rise", new SimpleObjectRef("ref-2", "against"))).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target).containsEntry("mapValueRef", new Document("beastie", "ref-1").append("rise", "ref-2"));
+ }
+
+ @Test // GH-3602
+ void updateReferenceMapWithValue() {
+
+ String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
+
+ CollectionRefRoot root = new CollectionRefRoot();
+ root.id = "root-1";
+ root.mapValueRef = Collections.singletonMap("beastie", new SimpleObjectRef("ref-1", "boys"));
+
+ template.save(root);
+
+ template.update(CollectionRefRoot.class).apply(new Update().set("mapValueRef.rise", "ref-2")).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
+ });
+
+ assertThat(target).containsEntry("mapValueRef", new Document("beastie", "ref-1").append("rise", "ref-2"));
+ }
+
+ @Test // GH-3602
+ void useReadingWriterConverterPairForLoading() {
+
+ SingleRefRoot root = new SingleRefRoot();
+ root.id = "root-1";
+ root.withReadingConverter = new SimpleObjectRefWithReadingConverter("ref-1", "value-1");
+
+ template.save(root.withReadingConverter);
+
+ template.save(root);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(template.getCollectionName(SingleRefRoot.class)).find(Filters.eq("_id", root.id)).first();
+ });
+
+ assertThat(target).containsEntry("withReadingConverter",
+ new Document("ref-key-from-custom-write-converter", root.withReadingConverter.id));
+
+ SingleRefRoot loaded = template.findOne(query(where("id").is(root.id)), SingleRefRoot.class);
+ assertThat(loaded.withReadingConverter).isInstanceOf(SimpleObjectRefWithReadingConverter.class);
+ }
+
+ @Test // GH-3602
+ void deriveMappingFromLookup() {
+
+ Publisher publisher = new Publisher();
+ publisher.id = "p-1";
+ publisher.acronym = "TOR";
+ publisher.name = "Tom Doherty Associates";
+
+ template.save(publisher);
+
+ Book book = new Book();
+ book.id = "book-1";
+ book.publisher = publisher;
+
+ template.save(book);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(template.getCollectionName(Book.class)).find(Filters.eq("_id", book.id)).first();
+ });
+
+ assertThat(target).containsEntry("publisher", new Document("acc", publisher.acronym).append("n", publisher.name));
+
+ Book result = template.findOne(query(where("id").is(book.id)), Book.class);
+ assertThat(result.publisher).isNotNull();
+ }
+
+ @Test // GH-3602
+ void updateDerivedMappingFromLookup() {
+
+ Publisher publisher = new Publisher();
+ publisher.id = "p-1";
+ publisher.acronym = "TOR";
+ publisher.name = "Tom Doherty Associates";
+
+ template.save(publisher);
+
+ Book book = new Book();
+ book.id = "book-1";
+
+ template.save(book);
+
+ template.update(Book.class).matching(where("id").is(book.id)).apply(new Update().set("publisher", publisher))
+ .first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(template.getCollectionName(Book.class)).find(Filters.eq("_id", book.id)).first();
+ });
+
+ assertThat(target).containsEntry("publisher", new Document("acc", publisher.acronym).append("n", publisher.name));
+
+ Book result = template.findOne(query(where("id").is(book.id)), Book.class);
+ assertThat(result.publisher).isNotNull();
+ }
+
+ @Test // GH-3602
+ void queryDerivedMappingFromLookup() {
+
+ Publisher publisher = new Publisher();
+ publisher.id = "p-1";
+ publisher.acronym = "TOR";
+ publisher.name = "Tom Doherty Associates";
+
+ template.save(publisher);
+
+ Book book = new Book();
+ book.id = "book-1";
+ book.publisher = publisher;
+
+ template.save(book);
+ book.publisher = publisher;
+
+ Book result = template.findOne(query(where("publisher").is(publisher)), Book.class);
+ assertThat(result.publisher).isNotNull();
+ }
+
+ @Test // GH-3602
+ void allowsDirectUsageOfAtReference() {
+
+ Publisher publisher = new Publisher();
+ publisher.id = "p-1";
+ publisher.acronym = "TOR";
+ publisher.name = "Tom Doherty Associates";
+
+ template.save(publisher);
+
+ UsingAtReference root = new UsingAtReference();
+ root.id = "book-1";
+ root.publisher = publisher;
+
+ template.save(root);
+
+ Document target = template.execute(db -> {
+ return db.getCollection(template.getCollectionName(UsingAtReference.class)).find(Filters.eq("_id", root.id)).first();
+ });
+
+ assertThat(target).containsEntry("publisher", "p-1");
+
+ UsingAtReference result = template.findOne(query(where("id").is(root.id)), UsingAtReference.class);
+ assertThat(result.publisher).isNotNull();
+ }
+
+ @Test // GH-3602
+ void updateWhenUsingAtReferenceDirectly() {
+
+ Publisher publisher = new Publisher();
+ publisher.id = "p-1";
+ publisher.acronym = "TOR";
+ publisher.name = "Tom Doherty Associates";
+
+ template.save(publisher);
+
+ UsingAtReference root = new UsingAtReference();
+ root.id = "book-1";
+
+ template.save(root);
+ template.update(UsingAtReference.class).matching(where("id").is(root.id)).apply(new Update().set("publisher", publisher)).first();
+
+ Document target = template.execute(db -> {
+ return db.getCollection(template.getCollectionName(UsingAtReference.class)).find(Filters.eq("_id", root.id)).first();
+ });
+
+ assertThat(target).containsEntry("publisher", "p-1");
+
+ }
+
+ @Data
+ static class SingleRefRoot {
+
+ String id;
+ String value;
+
+ @DocumentReference SimpleObjectRefWithReadingConverter withReadingConverter;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
+ SimpleObjectRef simpleValueRef;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }", lazy = true) //
+ SimpleObjectRef simpleLazyValueRef;
+
+ @Field("simple-value-ref-annotated-field-name") //
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
+ SimpleObjectRef simpleValueRefWithAnnotatedFieldName;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
+ ObjectRefOfDocument objectValueRef;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "#collection") //
+ ObjectRefOfDocumentWithEmbeddedCollectionName objectValueRefWithEmbeddedCollectionName;
+
+ @DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
+ ObjectRefOnNonIdField objectValueRefOnNonIdFields;
+
+ @DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }", lazy = true) //
+ ObjectRefOnNonIdField lazyObjectValueRefOnNonIdFields;
+ }
+
+ @Data
+ static class CollectionRefRoot {
+
+ String id;
+ String value;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
+ List simpleValueRef;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }", sort = "{ '_id' : -1 } ") //
+ List simpleSortedValueRef;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
+ Map mapValueRef;
+
+ @Field("simple-value-ref-annotated-field-name") //
+ @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
+ List simpleValueRefWithAnnotatedFieldName;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
+ List objectValueRef;
+
+ @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") //
+ List objectValueRefWithEmbeddedCollectionName;
+
+ @DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
+ List objectValueRefOnNonIdFields;
+ }
+
+ @FunctionalInterface
+ interface ReferenceAble {
+ Object toReference();
+ }
+
+ @Data
+ @AllArgsConstructor
+ @org.springframework.data.mongodb.core.mapping.Document("simple-object-ref")
+ static class SimpleObjectRef {
+
+ @Id String id;
+ String value;
+ }
+
+ @Getter
+ @Setter
+ static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
+
+ public SimpleObjectRefWithReadingConverter(String id, String value) {
+ super(id, value);
+ }
+
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class ObjectRefOfDocument implements ReferenceAble {
+
+ @Id String id;
+ String value;
+
+ @Override
+ public Object toReference() {
+ return new Document("id", id).append("property", "without-any-meaning");
+ }
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class ObjectRefOfDocumentWithEmbeddedCollectionName implements ReferenceAble {
+
+ @Id String id;
+ String value;
+
+ @Override
+ public Object toReference() {
+ return new Document("id", id).append("collection", "object-ref-of-document-with-embedded-collection-name");
+ }
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class ObjectRefOnNonIdField implements ReferenceAble {
+
+ @Id String id;
+ String value;
+ String refKey1;
+ String refKey2;
+
+ @Override
+ public Object toReference() {
+ return new Document("refKey1", refKey1).append("refKey2", refKey2);
+ }
+ }
+
+ static class ReferencableConverter implements Converter {
+
+ @Nullable
+ @Override
+ public DocumentPointer convert(ReferenceAble source) {
+ return source::toReference;
+ }
+ }
+
+ @WritingConverter
+ class DocumentToSimpleObjectRefWithReadingConverter
+ implements Converter, SimpleObjectRefWithReadingConverter> {
+
+ @Nullable
+ @Override
+ public SimpleObjectRefWithReadingConverter convert(DocumentPointer source) {
+
+ Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref")
+ .find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first();
+ return new SimpleObjectRefWithReadingConverter(document.getString("_id"), document.getString("value"));
+ }
+ }
+
+ @WritingConverter
+ class SimpleObjectRefWithReadingConverterToDocumentConverter
+ implements Converter> {
+
+ @Nullable
+ @Override
+ public DocumentPointer convert(SimpleObjectRefWithReadingConverter source) {
+ return () -> new Document("ref-key-from-custom-write-converter", source.getId());
+ }
+ }
+
+ @Getter
+ @Setter
+ static class WithRefA/* to B */ implements ReferenceAble {
+
+ @Id String id;
+ @DocumentReference //
+ WithRefB toB;
+
+ @Override
+ public Object toReference() {
+ return id;
+ }
+ }
+
+ @Getter
+ @Setter
+ @ToString
+ static class WithRefB/* to A */ implements ReferenceAble {
+
+ @Id String id;
+ @DocumentReference(lazy = true) //
+ WithRefA lazyToA;
+
+ @DocumentReference //
+ WithRefA eagerToA;
+
+ @Override
+ public Object toReference() {
+ return id;
+ }
+ }
+
+ static class ReferencedObject {}
+
+ class ToDocumentPointerConverter implements Converter> {
+
+ @Nullable
+ @Override
+ public DocumentPointer convert(ReferencedObject source) {
+ return () -> new Document("", source);
+ }
+ }
+
+ @Data
+ static class Book {
+
+ String id;
+
+ @DocumentReference(lookup = "{ 'acronym' : ?#{acc}, 'name' : ?#{n} }") //
+ Publisher publisher;
+
+ }
+
+ static class Publisher {
+
+ String id;
+ String acronym;
+ String name;
+ }
+
+ @Data
+ static class UsingAtReference {
+
+ String id;
+
+ @Reference //
+ Publisher publisher;
+ }
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
index 2c0f8649e2..84e7e2c2d8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
@@ -115,6 +115,8 @@ public void convertDocumentWithMapDBRef() {
when(dbMock.getCollection(anyString(), eq(Document.class))).thenReturn(collectionMock);
FindIterable fi = mock(FindIterable.class);
+ when(fi.limit(anyInt())).thenReturn(fi);
+ when(fi.sort(any())).thenReturn(fi);
when(fi.first()).thenReturn(mapValDocument);
when(collectionMock.find(Mockito.any(Bson.class))).thenReturn(fi);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java
new file mode 100644
index 0000000000..6990ddfe88
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java
@@ -0,0 +1,139 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.*;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+import org.bson.Document;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.mongodb.core.convert.DocumentPointerFactory.LinkageDocument;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+
+/**
+ * @author Christoph Strobl
+ */
+public class DocumentPointerFactoryUnitTests {
+
+ @Test // GH-3602
+ void errorsOnMongoOperatorUsage() {
+
+ LinkageDocument source = LinkageDocument.from("{ '_id' : { '$eq' : 1 } }");
+
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> getPointerValue(source, new Book())) //
+ .withMessageContaining("$eq");
+ }
+
+ @Test // GH-3602
+ void computesStaticPointer() {
+
+ LinkageDocument source = LinkageDocument.from("{ '_id' : 1 }");
+
+ assertThat(getPointerValue(source, new Book())).isEqualTo(new Document("_id", 1));
+ }
+
+ @Test // GH-3602
+ void computesPointerWithIdValuePlaceholder() {
+
+ LinkageDocument source = LinkageDocument.from("{ '_id' : ?#{id} }");
+
+ assertThat(getPointerValue(source, new Book("book-1", null, null))).isEqualTo(new Document("id", "book-1"));
+ }
+
+ @Test // GH-3602
+ void computesPointerForNonIdValuePlaceholder() {
+
+ LinkageDocument source = LinkageDocument.from("{ 'title' : ?#{book_title} }");
+
+ assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null)))
+ .isEqualTo(new Document("book_title", "Living With A Seal"));
+ }
+
+ @Test // GH-3602
+ void computesPlaceholderFromNestedPathValue() {
+
+ LinkageDocument source = LinkageDocument.from("{ 'metadata.pages' : ?#{p} } }");
+
+ assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null, new Metadata(272))))
+ .isEqualTo(new Document("p", 272));
+ }
+
+ @Test // GH-3602
+ void computesNestedPlaceholderPathValue() {
+
+ LinkageDocument source = LinkageDocument.from("{ 'metadata' : { 'pages' : ?#{metadata.pages} } }");
+
+ assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null, new Metadata(272))))
+ .isEqualTo(new Document("metadata", new Document("pages", 272)));
+ }
+
+ Object getPointerValue(LinkageDocument linkageDocument, Object value) {
+
+ MongoMappingContext mappingContext = new MongoMappingContext();
+ MongoPersistentEntity> persistentEntity = mappingContext.getPersistentEntity(value.getClass());
+ return linkageDocument
+ .getDocumentPointer(mappingContext, persistentEntity, persistentEntity.getPropertyPathAccessor(value))
+ .getPointer();
+ }
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ static class Book {
+ String id;
+ String title;
+ List author;
+ Metadata metadata;
+
+ public Book(String id, String title, List author) {
+ this.id = id;
+ this.title = title;
+ this.author = author;
+ }
+ }
+
+ static class Metadata {
+
+ int pages;
+
+ public Metadata(int pages) {
+ this.pages = pages;
+ }
+
+ public int getPages() {
+ return pages;
+ }
+
+ public void setPages(int pages) {
+ this.pages = pages;
+ }
+ }
+
+ @Data
+ static class Author {
+ String id;
+ String firstname;
+ String lastname;
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java
index 5006459fc8..91afb8c6ec 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java
@@ -17,9 +17,12 @@
import static org.assertj.core.api.Assertions.*;
+import java.util.function.Consumer;
+
import org.springframework.aop.framework.Advised;
import org.springframework.cglib.proxy.Factory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.LazyLoadingInterceptor;
+import org.springframework.data.mongodb.core.mapping.Unwrapped;
import org.springframework.test.util.ReflectionTestUtils;
/**
@@ -49,8 +52,36 @@ public static void assertProxyIsResolved(Object target, boolean expected) {
}
}
+ public static void assertProxy(Object proxy, Consumer verification) {
+
+ LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor = (LazyLoadingProxyFactory.LazyLoadingInterceptor) (proxy instanceof Advised
+ ? ((Advised) proxy).getAdvisors()[0].getAdvice()
+ : ((Factory) proxy).getCallback(0));
+
+ verification.accept(new LazyLoadingProxyValueRetriever(interceptor));
+ }
+
private static LazyLoadingInterceptor extractInterceptor(Object proxy) {
return (LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice()
: ((Factory) proxy).getCallback(0));
}
+
+ public static class LazyLoadingProxyValueRetriever {
+
+ LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor;
+
+ public LazyLoadingProxyValueRetriever(LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor) {
+ this.interceptor = interceptor;
+ }
+
+ public boolean isResolved() {
+ return (boolean) ReflectionTestUtils.getField(interceptor, "resolved");
+ }
+
+ @Unwrapped.Nullable
+ public Object currentValue() {
+ return ReflectionTestUtils.getField(interceptor, "result");
+ }
+
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
index a3836fd8b3..bd3e98788f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
@@ -580,9 +580,9 @@ void writesMapsOfObjectsCorrectly() {
org.bson.Document map = (org.bson.Document) field;
Object foo = map.get("Foo");
- assertThat(foo).isInstanceOf(BasicDBList.class);
+ assertThat(foo).isInstanceOf(List.class);
- BasicDBList value = (BasicDBList) foo;
+ List value = (List) foo;
assertThat(value.size()).isEqualTo(1);
assertThat(value.get(0)).isEqualTo("Bar");
}
@@ -695,9 +695,9 @@ void writesPlainMapOfCollectionsCorrectly() {
assertThat(result.containsKey("Foo")).isTrue();
assertThat(result.get("Foo")).isNotNull();
- assertThat(result.get("Foo")).isInstanceOf(BasicDBList.class);
+ assertThat(result.get("Foo")).isInstanceOf(List.class);
- BasicDBList list = (BasicDBList) result.get("Foo");
+ List list = (List) result.get("Foo");
assertThat(list.size()).isEqualTo(1);
assertThat(list.get(0)).isEqualTo(Locale.US.toString());
@@ -744,7 +744,7 @@ void writesArraysAsMapValuesCorrectly() {
org.bson.Document map = (org.bson.Document) mapObject;
Object valueObject = map.get("foo");
- assertThat(valueObject).isInstanceOf(BasicDBList.class);
+ assertThat(valueObject).isInstanceOf(List.class);
List