diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index 96b6c6876..5277fbc0b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -46,7 +46,7 @@ import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.LazyLoadingException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisStd; @@ -117,7 +117,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db */ @Override public Document fetch(DBRef dbRef) { - return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef)); + return getReferenceLoader().fetch(DocumentReferenceQuery.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), + ReferenceCollection.fromDBRef(dbRef)); } /* @@ -157,9 +158,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db databaseSource.getCollectionName()); } - List result = getReferenceLoader() - .bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next())) - .collect(Collectors.toList()); + List result = mongoCollection // + .find(new Document("_id", new Document("$in", ids))) // + .into(new ArrayList<>()); return ids.stream() // .flatMap(id -> documentWithId(id, result)) // @@ -498,9 +499,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db .getCollection(dbref.getCollectionName(), Document.class); } - protected MongoCollection getCollection(ReferenceContext context) { + protected MongoCollection getCollection(ReferenceCollection context) { - return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(), Document.class); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java index 27feca163..66b698077 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java @@ -15,21 +15,15 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - import org.bson.Document; -import org.bson.conversions.Bson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; -import org.springframework.lang.Nullable; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; /** @@ -49,7 +43,7 @@ public class DefaultReferenceLoader implements ReferenceLoader { } @Override - public Stream bulkFetch(ReferenceFilter filter, ReferenceContext context) { + public Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context) { MongoCollection collection = getCollection(context); @@ -63,9 +57,9 @@ public class DefaultReferenceLoader implements ReferenceLoader { return filter.apply(collection); } - protected MongoCollection getCollection(ReferenceContext context) { + protected MongoCollection getCollection(ReferenceCollection context) { - return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(), Document.class); } } 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 b4324b505..0692f719b 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 @@ -15,12 +15,6 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.function.BiFunction; -import java.util.stream.Stream; - -import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; @@ -44,24 +38,26 @@ public class DefaultReferenceResolver implements ReferenceResolver { @Nullable @Override public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { if (isLazyReference(property)) { - return createLazyLoadingProxy(property, source, referenceReader, lookupFunction); + return createLazyLoadingProxy(property, source, referenceReader, lookupFunction, resultConversionFunction); } - return referenceReader.readReference(property, source, lookupFunction); + return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); } private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - ReferenceReader referenceReader, BiFunction> lookupFunction) { - return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction); + ReferenceReader referenceReader, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { + return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction, + resultConversionFunction); } protected boolean isLazyReference(MongoPersistentProperty property) { - if (property.findAnnotation(DocumentReference.class) != null) { - return property.findAnnotation(DocumentReference.class).lazy(); + if (property.isDocumentReference()) { + return property.getDocumentReference().lazy(); } return property.getDBRef() != null && property.getDBRef().lazy(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java new file mode 100644 index 000000000..a91a48d92 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -0,0 +1,135 @@ +/* + * 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 java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bson.Document; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; +import org.springframework.data.mongodb.core.mapping.DocumentPointer; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * @author Christoph Strobl + * @since 3.3 + */ +class DocumentPointerFactory { + + private ConversionService conversionService; + private MappingContext, MongoPersistentProperty> mappingContext; + private Map linkageMap; + + public DocumentPointerFactory(ConversionService conversionService, + MappingContext, MongoPersistentProperty> mappingContext) { + + this.conversionService = conversionService; + this.mappingContext = mappingContext; + this.linkageMap = new HashMap<>(); + } + + public DocumentPointer computePointer(MongoPersistentProperty property, Object value, Class typeHint) { + + if (value instanceof LazyLoadingProxy) { + return () -> ((LazyLoadingProxy) value).getSource(); + } + + if (conversionService.canConvert(typeHint, DocumentPointer.class)) { + return conversionService.convert(value, DocumentPointer.class); + } else { + + MongoPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(property.getAssociationTargetType()); + + if (!property.getDocumentReference().lookup().toLowerCase().replaceAll("\\s", "").replaceAll("'", "") + .equals("{_id:?#{#target}}")) { + + return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), key -> { + return new LinkageDocument(key); + }).get(persistentEntity, + BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value)); + } + + // just take the id as a reference + return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); + } + } + + static class LinkageDocument { + + String lookup; + org.bson.Document fetchDocument; + Map mapMap; + + public LinkageDocument(String lookup) { + + this.lookup = lookup; + String targetLookup = lookup; + + Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); + + Matcher matcher = pattern.matcher(lookup); + int index = 0; + mapMap = new LinkedHashMap<>(); + while (matcher.find()) { + + String expr = matcher.group(); + mapMap.put(Integer.valueOf(index), expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") + .replace("target.", "").replaceAll("'", "")); + targetLookup = targetLookup.replace(expr, index + ""); + index++; + } + + fetchDocument = org.bson.Document.parse(targetLookup); + } + + org.bson.Document get(MongoPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor) { + + org.bson.Document targetDocument = new Document(); + + // TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing? + // like we have it ordered by index values and could provide the parameter array from it. + + for (Entry entry : fetchDocument.entrySet()) { + + if (entry.getKey().equals("target")) { + + String refKey = mapMap.get(entry.getValue()); + + if (persistentEntity.hasIdProperty()) { + targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty())); + } else { + targetDocument.put(refKey, propertyAccessor.getBean()); + } + continue; + } + + Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey())); + String refKey = mapMap.get(entry.getValue()); + targetDocument.put(refKey, target); + } + return targetDocument; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java index a04a100cc..8be711198 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java @@ -46,4 +46,15 @@ public interface LazyLoadingProxy { */ @Nullable DBRef toDBRef(); + + /** + * Returns the raw {@literal source} object that defines the reference. + * + * @return can be {@literal null}. + * @since 3.3 + */ + @Nullable + default Object getSource() { + return toDBRef(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java index 35da1e1e2..570a516d9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java @@ -19,23 +19,19 @@ import static org.springframework.util.ReflectionUtils.*; import java.io.Serializable; import java.lang.reflect.Method; -import java.util.function.BiFunction; -import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.bson.Document; -import org.bson.conversions.Bson; import org.springframework.aop.framework.ProxyFactory; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.objenesis.ObjenesisStd; import org.springframework.util.ReflectionUtils; @@ -54,11 +50,12 @@ class LazyLoadingProxyGenerator { this.objenesis = new ObjenesisStd(true); } - public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - BiFunction> lookupFunction) { + public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { Class propertyType = property.getType(); - LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction); + LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction, + resultConversionFunction); if (!propertyType.isInterface()) { @@ -105,27 +102,30 @@ class LazyLoadingProxyGenerator { private volatile boolean resolved; private @org.springframework.lang.Nullable Object result; private Object source; - private BiFunction> lookupFunction; + private LookupFunction lookupFunction; + private ResultConversionFunction resultConversionFunction; - private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD; + private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD, GET_SOURCE_METHOD; { try { INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget"); TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef"); FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize"); + GET_SOURCE_METHOD = LazyLoadingProxy.class.getMethod("getSource"); } catch (Exception e) { throw new RuntimeException(e); } } public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { this.property = property; this.source = source; this.referenceReader = reader; this.lookupFunction = lookupFunction; + this.resultConversionFunction = resultConversionFunction; } @Nullable @@ -145,6 +145,10 @@ class LazyLoadingProxyGenerator { return null; } + if (GET_SOURCE_METHOD.equals(method)) { + return source; + } + if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) { if (ReflectionUtils.isToStringMethod(method)) { @@ -234,7 +238,7 @@ class LazyLoadingProxyGenerator { // property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()); // } - return referenceReader.readReference(property, source, lookupFunction); + return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); } catch (RuntimeException ex) { throw ex; 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 0d3378d39..2ad4d7523 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 @@ -63,10 +63,9 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.core.mapping.DocumentReference; +import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.ObjectReference; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; @@ -124,6 +123,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; + private DocumentPointerFactory documentPointerFactory; /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -154,8 +154,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator); }); - this.referenceReader = new ReferenceReader(mappingContext, - (prop, document) -> this.read(prop.getActualType(), document), () -> spELContext); + this.referenceReader = new ReferenceReader(mappingContext, () -> spELContext); + this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext); } /** @@ -366,6 +366,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); DocumentAccessor documentAccessor = new DocumentAccessor(bson); + if (bson.get("_id") != null) { + + Object existing = context.getPath().getPathItem(bson.get("_id"), entity.getCollection(), entity.getType()); + if (existing != null) { + return (S) existing; + } + } + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); ParameterValueProvider provider = persistenceConstructor != null @@ -376,6 +384,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App S instance = instantiator.createInstance(entity, provider); if (entity.requiresPropertyPopulation()) { + return populateProperties(context, entity, documentAccessor, evaluator, instance); } @@ -451,7 +460,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + evaluator); continue; } @@ -478,7 +488,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + evaluator); continue; } @@ -494,7 +505,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } private void readAssociation(Association association, PersistentPropertyAccessor accessor, - DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) { + DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback, + ConversionContext context, SpELExpressionEvaluator evaluator) { MongoPersistentProperty property = association.getInverse(); final Object value = documentAccessor.get(property); @@ -503,26 +515,32 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return; } - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { // quite unusual but sounds like worth having? - if (conversionService.canConvert(ObjectReference.class, property.getActualType())) { + if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) { - // collection like special treatment - accessor.setProperty(property, conversionService.convert(new ObjectReference() { + DocumentPointer pointer = new DocumentPointer() { @Override public Object getPointer() { return value; } - }, property.getActualType())); + }; + + // collection like special treatment + accessor.setProperty(property, conversionService.convert(pointer, property.getActualType())); } else { - accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader)); + accessor.setProperty(property, + dbRefResolver.resolveReference(property, value, referenceReader, context::convert)); } return; } DBRef dbref = value instanceof DBRef ? (DBRef) value : null; + + // TODO: accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader, + // context::convert)); accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler)); } @@ -563,6 +581,45 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return createDBRef(object, referringProperty); } + public Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { + + if (source instanceof LazyLoadingProxy) { + return ((LazyLoadingProxy) source).getSource(); + } + + if (referringProperty != null) { + + if (referringProperty.isDbReference()) { + return toDBRef(source, referringProperty); + } + if (referringProperty.isDocumentReference()) { + return createDocumentPointer(source, referringProperty); + } + } + + throw new RuntimeException("oops - what's that " + source); + } + + Object createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { + + if (referringProperty == null) { + return source; + } + + if (ClassUtils.isAssignableValue(referringProperty.getType(), source) + && conversionService.canConvert(referringProperty.getType(), DocumentPointer.class)) { + return conversionService.convert(source, DocumentPointer.class).getPointer(); + } + + if (ClassUtils.isAssignableValue(referringProperty.getAssociationTargetType(), source)) { + return documentPointerFactory.computePointer(referringProperty, source, referringProperty.getActualType()) + .getPointer(); + + } + + return source; + } + /** * Root entry method into write conversion. Adds a type discriminator to the {@link Document}. Shouldn't be called for * nested conversions. @@ -749,13 +806,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (prop.isAssociation()) { - if (conversionService.canConvert(valueType.getType(), ObjectReference.class)) { - accessor.put(prop, conversionService.convert(obj, ObjectReference.class).getPointer()); - } else { - // just take the id as a reference - accessor.put(prop, mappingContext.getPersistentEntity(prop.getAssociationTargetType()) - .getIdentifierAccessor(obj).getIdentifier()); - } + accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext) + .computePointer(prop, obj, valueType.getType()).getPointer()); return; } @@ -799,14 +851,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (property.isAssociation()) { return writeCollectionInternal(collection.stream().map(it -> { - if (conversionService.canConvert(it.getClass(), ObjectReference.class)) { - return conversionService.convert(it, ObjectReference.class).getPointer(); + if (conversionService.canConvert(it.getClass(), DocumentPointer.class)) { + return conversionService.convert(it, DocumentPointer.class).getPointer(); } else { // just take the id as a reference return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it) .getIdentifier(); } - }).collect(Collectors.toList()), ClassTypeInformation.from(ObjectReference.class), new BasicDBList()); + }).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new BasicDBList()); } if (property.hasExplicitWriteTarget()) { @@ -855,15 +907,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (conversions.isSimpleType(key.getClass())) { String simpleKey = prepareMapKey(key.toString()); - if(property.isDbReference()) { + if (property.isDbReference()) { document.put(simpleKey, value != null ? createDBRef(value, property) : null); } else { - if (conversionService.canConvert(value.getClass(), ObjectReference.class)) { - document.put(simpleKey, conversionService.convert(value, ObjectReference.class).getPointer()); + if (conversionService.canConvert(value.getClass(), DocumentPointer.class)) { + document.put(simpleKey, conversionService.convert(value, DocumentPointer.class).getPointer()); } else { // just take the id as a reference - document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value) - .getIdentifier()); + document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()) + .getIdentifierAccessor(value).getIdentifier()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index 0f64177bc..779b3236d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -70,4 +70,8 @@ public interface MongoWriter extends EntityWriter { * @return will never be {@literal null}. */ DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty); + + default Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { + return toDBRef(source, referringProperty); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java index cbd02ee74..8b6c96943 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java @@ -20,9 +20,9 @@ import java.util.function.BiFunction; import java.util.stream.Stream; import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -77,7 +77,8 @@ public enum NoOpDbRefResolver implements DbRefResolver { @Nullable @Override public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index af93fdd63..36353e4f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -605,7 +605,7 @@ public class QueryMapper { if (source instanceof Iterable) { BasicDBList result = new BasicDBList(); for (Object element : (Iterable) source) { - result.add(createDbRefFor(element, property)); + result.add(createReferenceFor(element, property)); } return result; } @@ -614,12 +614,12 @@ public class QueryMapper { Document result = new Document(); Document dbObject = (Document) source; for (String key : dbObject.keySet()) { - result.put(key, createDbRefFor(dbObject.get(key), property)); + result.put(key, createReferenceFor(dbObject.get(key), property)); } return result; } - return createDbRefFor(source, property); + return createReferenceFor(source, property); } /** @@ -666,12 +666,16 @@ public class QueryMapper { return Collections.singletonMap(key, value).entrySet().iterator().next(); } - private DBRef createDbRefFor(Object source, MongoPersistentProperty property) { + private Object createReferenceFor(Object source, MongoPersistentProperty property) { if (source instanceof DBRef) { return (DBRef) source; } + if(property != null && property.isDocumentReference()) { + return converter.toDocumentReference(source, property); + } + return converter.toDBRef(source, property); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java index 184918529..d5c72afad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java @@ -15,12 +15,12 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.Collections; +import java.util.Iterator; import org.bson.Document; import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.lang.Nullable; import com.mongodb.client.MongoCollection; @@ -31,15 +31,15 @@ import com.mongodb.client.MongoCollection; public interface ReferenceLoader { @Nullable - default Document fetch(ReferenceFilter filter, ReferenceContext context) { - return bulkFetch(filter, context).findFirst().orElse(null); + default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) { + + Iterator it = bulkFetch(filter, context).iterator(); + return it.hasNext() ? it.next() : null; } - // meh, Stream! - Stream bulkFetch(ReferenceFilter filter, ReferenceContext context); + Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context); - // Reference query - interface ReferenceFilter { + interface DocumentReferenceQuery { Bson getFilter(); @@ -49,21 +49,21 @@ public interface ReferenceLoader { // TODO: Move apply method into something else that holds the collection and knows about single item/multi-item // processing - default Stream apply(MongoCollection collection) { - return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false)); + default Iterable apply(MongoCollection collection) { + return restoreOrder(collection.find(getFilter()).sort(getSort())); } - - default Stream restoreOrder(Stream stream) { - return stream; + + default Iterable restoreOrder(Iterable documents) { + return documents; } - static ReferenceFilter referenceFilter(Bson bson) { + static DocumentReferenceQuery referenceFilter(Bson bson) { return () -> bson; } - static ReferenceFilter singleReferenceFilter(Bson bson) { + static DocumentReferenceQuery singleReferenceFilter(Bson bson) { - return new ReferenceFilter() { + return new DocumentReferenceQuery() { @Override public Bson getFilter() { @@ -71,10 +71,10 @@ public interface ReferenceLoader { } @Override - public Stream apply(MongoCollection collection) { + public Iterable apply(MongoCollection collection) { Document result = collection.find(getFilter()).sort(getSort()).limit(1).first(); - return result != null ? Stream.of(result) : Stream.empty(); + return result != null ? Collections.singleton(result) : Collections.emptyList(); } }; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java index e5a16ea43..fb37367b1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java @@ -17,24 +17,24 @@ package org.springframework.data.mongodb.core.convert; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.bson.Document; import org.bson.conversions.Bson; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -56,61 +56,47 @@ import com.mongodb.client.MongoCollection; */ public class ReferenceReader { - private final ParameterBindingDocumentCodec codec; - private final Lazy, MongoPersistentProperty>> mappingContext; - private final BiFunction documentConversionFunction; private final Supplier spelContextSupplier; + private final ParameterBindingDocumentCodec codec; public ReferenceReader(MappingContext, MongoPersistentProperty> mappingContext, - BiFunction documentConversionFunction, Supplier spelContextSupplier) { - this(() -> mappingContext, documentConversionFunction, spelContextSupplier); + this(() -> mappingContext, spelContextSupplier); } public ReferenceReader( Supplier, MongoPersistentProperty>> mappingContextSupplier, - BiFunction documentConversionFunction, Supplier spelContextSupplier) { this.mappingContext = Lazy.of(mappingContextSupplier); - this.documentConversionFunction = documentConversionFunction; this.spelContextSupplier = spelContextSupplier; this.codec = new ParameterBindingDocumentCodec(); } - // TODO: Move documentConversionFunction to here. Having a contextual read allows projections in references - Object readReference(MongoPersistentProperty property, Object value, - BiFunction> lookupFunction) { + Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { SpELContext spELContext = spelContextSupplier.get(); - ReferenceFilter filter = computeFilter(property, value, spELContext); - ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext); + DocumentReferenceQuery filter = computeFilter(property, value, spELContext); + ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext); - Stream result = lookupFunction.apply(referenceContext, filter); + Iterable result = lookupFunction.apply(filter, referenceCollection); - if (property.isCollectionLike()) { - return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList()); + if (!result.iterator().hasNext()) { + return null; } - // TODO: retain target type and extract types here so the conversion function doesn't require type fiddling - // BiFunction instead of MongoPersistentProperty - if (property.isMap()) { - - // the order is a real problem here - Iterator keyIterator = ((Map) value).keySet().iterator(); - return result.map(it -> it.entrySet().stream().collect(Collectors.toMap(key -> key.getKey(), val -> { - Object apply = documentConversionFunction.apply(property, (Document) val.getValue()); - return apply; - }))).findFirst().orElse(null); + if (property.isCollectionLike()) { + return resultConversionFunction.apply(result, property.getTypeInformation()); } - return result.map(it -> documentConversionFunction.apply(property, it)).findFirst().orElse(null); + return resultConversionFunction.apply(result.iterator().next(), property.getTypeInformation()); } - private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value, + private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, SpELContext spELContext) { if (value instanceof Iterable) { @@ -118,44 +104,43 @@ public class ReferenceReader { } if (value instanceof DBRef) { - return ReferenceContext.fromDBRef((DBRef) value); + return ReferenceCollection.fromDBRef((DBRef) value); } if (value instanceof Document) { Document ref = (Document) value; - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> ref.get("db", String.class)); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> ref.get("collection", mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); - return new ReferenceContext(targetDatabase, targetCollection); + return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceContext(ref.getString("db"), ref.get("collection", + return new ReferenceCollection(ref.getString("db"), ref.get("collection", mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); } - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); - Document sort = parseValueOrGet(documentReference.sort(), bindingContext, () -> null); - return new ReferenceContext(targetDatabase, targetCollection); + return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceContext(null, + return new ReferenceCollection(null, mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); } @@ -201,9 +186,9 @@ public class ReferenceReader { return ctx; } - ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { + DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String lookup = documentReference.lookup(); Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null); @@ -217,7 +202,7 @@ public class ReferenceReader { ors.add(decoded); } - return new ListReferenceFilter(new Document("$or", ors), sort); + return new ListDocumentReferenceQuery(new Document("$or", ors), sort); } if (property.isMap() && value instanceof Map) { @@ -230,18 +215,18 @@ public class ReferenceReader { filterMap.put(entry.getKey(), decoded); } - return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap); + return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap); } - return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); + return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); } - static class SingleReferenceFilter implements ReferenceFilter { + static class SingleDocumentReferenceQuery implements DocumentReferenceQuery { Document filter; Document sort; - public SingleReferenceFilter(Document filter, Document sort) { + public SingleDocumentReferenceQuery(Document filter, Document sort) { this.filter = filter; this.sort = sort; } @@ -252,24 +237,24 @@ public class ReferenceReader { } @Override - public Stream apply(MongoCollection collection) { + public Iterable apply(MongoCollection collection) { Document result = collection.find(getFilter()).limit(1).first(); - return result != null ? Stream.of(result) : Stream.empty(); + return result != null ? Collections.singleton(result) : Collections.emptyList(); } } - static class MapReferenceFilter implements ReferenceFilter { + static class MapDocumentReferenceQuery implements DocumentReferenceQuery { - Document filter; - Document sort; - Map filterOrderMap; + private final Document filter; + private final Document sort; + private final Map filterOrderMap; - public MapReferenceFilter(Document filter, Document sort, Map filterOrderMap) { + public MapDocumentReferenceQuery(Document filter, Document sort, Map filterOrderMap) { this.filter = filter; - this.filterOrderMap = filterOrderMap; this.sort = sort; + this.filterOrderMap = filterOrderMap; } @Override @@ -283,45 +268,46 @@ public class ReferenceReader { } @Override - public Stream restoreOrder(Stream stream) { + public Iterable restoreOrder(Iterable documents) { Map targetMap = new LinkedHashMap<>(); - List collected = stream.collect(Collectors.toList()); + List collected = documents instanceof List ? (List) documents + : Streamable.of(documents).toList(); for (Entry filterMapping : filterOrderMap.entrySet()) { - String key = filterMapping.getKey().toString(); - Optional first = collected.stream().filter(it -> { + Optional first = collected.stream() + .filter(it -> it.entrySet().containsAll(filterMapping.getValue().entrySet())).findFirst(); - boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet()); - return found; - }).findFirst(); - - targetMap.put(key, first.orElse(null)); + targetMap.put(filterMapping.getKey().toString(), first.orElse(null)); } - return Stream.of(new Document(targetMap)); + return Collections.singleton(new Document(targetMap)); } } - static class ListReferenceFilter implements ReferenceFilter { + static class ListDocumentReferenceQuery implements DocumentReferenceQuery { - Document filter; - Document sort; + private final Document filter; + private final Document sort; + + public ListDocumentReferenceQuery(Document filter, Document sort) { - public ListReferenceFilter(Document filter, Document sort) { this.filter = filter; this.sort = sort; } @Override - public Stream restoreOrder(Stream stream) { + public Iterable restoreOrder(Iterable documents) { if (filter.containsKey("$or")) { List ors = filter.get("$or", List.class); - return stream.sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)); + List target = documents instanceof List ? (List) documents + : Streamable.of(documents).toList(); + return target.stream().sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)) + .collect(Collectors.toList()); } - return stream; + return documents; } public Document getFilter() { @@ -347,7 +333,5 @@ public class ReferenceReader { } return referenceList.size(); } - } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java index 50bc6558d..f29dc16a7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java @@ -15,13 +15,12 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.function.BiFunction; -import java.util.stream.Stream; +import java.util.Collections; import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -33,34 +32,38 @@ public interface ReferenceResolver { @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction); + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction); - default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader) { - return resolveReference(property, source, referenceReader, (ctx, filter) -> { + default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, + ResultConversionFunction resultConversionFunction) { + + return resolveReference(property, source, referenceReader, (filter, ctx) -> { if (property.isCollectionLike() || property.isMap()) { return getReferenceLoader().bulkFetch(filter, ctx); + } + Object target = getReferenceLoader().fetch(filter, ctx); - return target == null ? Stream.empty() : Stream.of(getReferenceLoader().fetch(filter, ctx)); - }); + return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetch(filter, ctx)); + }, resultConversionFunction); } ReferenceLoader getReferenceLoader(); - // TODO: ReferenceCollection - class ReferenceContext { + class ReferenceCollection { - @Nullable final String database; - final String collection; + @Nullable + private final String database; + private final String collection; - public ReferenceContext(@Nullable String database, String collection) { + public ReferenceCollection(@Nullable String database, String collection) { this.database = database; this.collection = collection; } - static ReferenceContext fromDBRef(DBRef dbRef) { - return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName()); + static ReferenceCollection fromDBRef(DBRef dbRef) { + return new ReferenceCollection(dbRef.getDatabaseName(), dbRef.getCollectionName()); } public String getCollection() { @@ -72,4 +75,14 @@ public interface ReferenceResolver { return database; } } + + @FunctionalInterface + interface LookupFunction { + Iterable apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection); + } + + @FunctionalInterface + interface ResultConversionFunction { + Object apply(Object source, TypeInformation property); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 0b47c79d0..b7b71a7fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -231,6 +231,15 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope return isAnnotationPresent(DBRef.class); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isDocumentReference() + */ + @Override + public boolean isDocumentReference() { + return isAnnotationPresent(DocumentReference.class); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef() @@ -240,6 +249,16 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope return findAnnotation(DBRef.class); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDocumentReference() + */ + @Nullable + @Override + public DocumentReference getDocumentReference() { + return findAnnotation(DocumentReference.class); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentPointer.java similarity index 57% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentPointer.java index 9904b20d3..de7fbff86 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentPointer.java @@ -16,10 +16,19 @@ package org.springframework.data.mongodb.core.mapping; /** + * A custom pointer to a linked document to be used along with {@link DocumentReference} for storing the linkage value. + * * @author Christoph Strobl */ @FunctionalInterface -// TODO: ObjectPointer or DocumentPointer -public interface ObjectReference { +public interface DocumentPointer { + + /** + * 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 index d9af6ccee..0846c4022 100644 --- 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 @@ -24,8 +24,69 @@ 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) @@ -34,17 +95,38 @@ import org.springframework.data.annotation.Reference; public @interface DocumentReference { /** - * The database the referred entity resides in. + * The database the linked entity resides in. * - * @return empty String by default. + * @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 7c347229b..c753f3856 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 { - it.customConverters(new ReferencableConverter()); + it.customConverters(new ReferencableConverter(), new SimpleObjectRefWithReadingConverterToDocumentConverter(), + new DocumentToSimpleObjectRefWithReadingConverter()); }); cfg.configureMappingContext(it -> { @@ -84,7 +87,7 @@ public class MongoTemplateDocumentReferenceTests { template.flushDatabase(); } - @Test + @Test // GH-3602 void writeSimpleTypeReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -102,12 +105,11 @@ public class MongoTemplateDocumentReferenceTests { assertThat(target.get("simpleValueRef")).isEqualTo("ref-1"); } - @Test + @Test // GH-3602 void writeMapTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); - CollectionRefRoot source = new CollectionRefRoot(); source.id = "root-1"; source.mapValueRef = new LinkedHashMap<>(); @@ -120,11 +122,10 @@ public class MongoTemplateDocumentReferenceTests { return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); }); - System.out.println("target: " + target.toJson()); assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2"); } - @Test + @Test // GH-3602 void writeCollectionOfSimpleTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -143,7 +144,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2"); } - @Test + @Test // GH-3602 void writeObjectTypeReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -161,7 +162,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference()); } - @Test + @Test // GH-3602 void writeCollectionOfObjectTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -181,7 +182,7 @@ public class MongoTemplateDocumentReferenceTests { source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference()); } - @Test + @Test // GH-3602 void readSimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -200,7 +201,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionOfSimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -220,7 +221,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readLazySimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -245,7 +246,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readSimpleTypeObjectReferenceFromFieldWithCustomName() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -266,7 +267,7 @@ public class MongoTemplateDocumentReferenceTests { .isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionTypeObjectReferenceFromFieldWithCustomName() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -287,7 +288,7 @@ public class MongoTemplateDocumentReferenceTests { .containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentType() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -307,7 +308,7 @@ public class MongoTemplateDocumentReferenceTests { assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentType() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -328,7 +329,7 @@ public class MongoTemplateDocumentReferenceTests { .containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentDeclaringCollectionName() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -351,7 +352,7 @@ public class MongoTemplateDocumentReferenceTests { .isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -379,7 +380,7 @@ public class MongoTemplateDocumentReferenceTests { new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -401,7 +402,7 @@ public class MongoTemplateDocumentReferenceTests { .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -429,7 +430,7 @@ public class MongoTemplateDocumentReferenceTests { .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -452,7 +453,7 @@ public class MongoTemplateDocumentReferenceTests { .containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readMapOfReferences() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -479,12 +480,414 @@ public class MongoTemplateDocumentReferenceTests { }); CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class); - System.out.println("result: " + result); - 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")); + 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(); } @Data @@ -556,16 +959,16 @@ public class MongoTemplateDocumentReferenceTests { @Id String id; String value; - } @Getter @Setter static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef { - public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) { + public SimpleObjectRefWithReadingConverter(String id, String value) { super(id, value); } + } @Data @@ -609,41 +1012,94 @@ public class MongoTemplateDocumentReferenceTests { } } - static class ReferencableConverter implements Converter { + static class ReferencableConverter implements Converter { @Nullable @Override - public ObjectReference convert(ReferenceAble source) { + public DocumentPointer convert(ReferenceAble source) { return source::toReference; } } @WritingConverter class DocumentToSimpleObjectRefWithReadingConverter - implements Converter, SimpleObjectRefWithReadingConverter> { + implements Converter, SimpleObjectRefWithReadingConverter> { - private final MongoTemplate template; + @Nullable + @Override + public SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { - public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) { - this.template = template; + 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 SimpleObjectRefWithReadingConverter convert(ObjectReference source) { - return template.findOne(query(where("id").is(source.getPointer().get("the-ref-key-you-did-not-expect"))), - SimpleObjectRefWithReadingConverter.class); + public DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { + return () -> new Document("ref-key-from-custom-write-converter", source.getId()); } } - @WritingConverter - class SimpleObjectRefWithReadingConverterToDocumentConverter - implements Converter> { + @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 ObjectReference convert(SimpleObjectRefWithReadingConverter source) { - return () -> new Document("the-ref-key-you-did-not-expect", source.getId()); + 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; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java index c0a6b8df9..d7a287047 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java @@ -33,6 +33,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -64,8 +65,6 @@ class DefaultDbRefResolverUnitTests { when(factoryMock.getMongoDatabase()).thenReturn(dbMock); when(dbMock.getCollection(anyString(), any(Class.class))).thenReturn(collectionMock); when(collectionMock.find(any(Document.class))).thenReturn(cursorMock); - when(cursorMock.sort(any(Document.class))).thenReturn(cursorMock); - when(cursorMock.spliterator()).thenReturn(Collections. emptyList().spliterator()); resolver = new DefaultDbRefResolver(factoryMock); } @@ -116,7 +115,7 @@ class DefaultDbRefResolverUnitTests { DBRef ref1 = new DBRef("collection-1", o1.get("_id")); DBRef ref2 = new DBRef("collection-1", o2.get("_id")); - when(cursorMock.spliterator()).thenReturn(Arrays.asList(o2, o1).spliterator()); + when(cursorMock.into(any())).then(invocation -> Arrays.asList(o2, o1)); assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(o1, o2); } @@ -129,7 +128,7 @@ class DefaultDbRefResolverUnitTests { DBRef ref1 = new DBRef("collection-1", document.get("_id")); DBRef ref2 = new DBRef("collection-1", document.get("_id")); - when(cursorMock.spliterator()).thenReturn(Arrays.asList(document).spliterator()); + when(cursorMock.into(any())).then(invocation -> Arrays.asList(document)); assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index e2f69260b..9c157db75 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -48,6 +48,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -1487,4 +1488,32 @@ public class QueryMapperUnitTests { @Field("renamed") String renamed_fieldname_with_underscores; } + + static class WithDocumentReferences { + + @DocumentReference + Sample sample; + + @DocumentReference + SimpeEntityWithoutId noId; + + @DocumentReference(lookup = "{ 'stringProperty' : ?#{stringProperty} }") + SimpeEntityWithoutId noIdButLookupQuery; + + } + + @Test + void xxx() { + + Sample sample = new Sample(); + sample.foo = "sample-id"; + + Query query = query(where("sample").is(sample)); + + org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), + context.getPersistentEntity(WithDocumentReferences.class)); + + System.out.println("mappedObject.toJson(): " + mappedObject.toJson()); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java index 9aa1bb0b5..b70930dae 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java @@ -18,11 +18,10 @@ package org.springframework.data.mongodb.performance; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; -import org.bson.conversions.Bson; import org.springframework.data.mongodb.core.convert.ReferenceLoader; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.convert.ReferenceReader; -import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -107,7 +106,7 @@ public class ReactivePerformanceTests { @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction> lookupFunction) { + public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { return null; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 9ab37e3ff..61caa3056 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1434,4 +1434,19 @@ public abstract class AbstractPersonRepositoryIntegrationTests { Person target = repository.findWithAggregationInProjection(alicia.getId()); assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase()); } + + @Test // GH-3602 + void executesQueryWithDocumentReferenceCorrectly() { + + Person josh = new Person("Josh", "Long"); + User dave = new User(); + dave.id = "dave"; + + josh.setSpiritAnimal(dave); + + operations.save(josh); + + List result = repository.findBySpiritAnimal(dave); + assertThat(result).map(Person::getId).containsExactly(josh.getId()); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 01b0c28de..62c5b18be 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Unwrapped; @@ -74,6 +75,9 @@ public class Person extends Contact { @Unwrapped.Nullable(prefix = "u") // User unwrappedUser; + @DocumentReference + User spiritAnimal; + public Person() { this(null, null); @@ -308,6 +312,14 @@ public class Person extends Contact { this.unwrappedUser = unwrappedUser; } + public User getSpiritAnimal() { + return spiritAnimal; + } + + public void setSpiritAnimal(User spiritAnimal) { + this.spiritAnimal = spiritAnimal; + } + /* * (non-Javadoc) * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 314655e78..ca382fa2c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -416,4 +416,6 @@ public interface PersonRepository extends MongoRepository, Query List findByUnwrappedUserUsername(String username); List findByUnwrappedUser(User user); + + List findBySpiritAnimal(User user); } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 03d18bacf..842dd8341 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New & Noteworthy +[[new-features.3.3]] +== What's New in Spring Data MongoDB 3.3 + +* Extended support for <> entities. + [[new-features.3.2]] == What's New in Spring Data MongoDB 3.2 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 82b5632f2..1998fe1ad 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -480,6 +480,7 @@ The MappingMongoConverter can use metadata to drive the mapping of objects to do * `@MongoId`: Applied at the field level to mark the field used for identity purpose. Accepts an optional `FieldType` to customize id conversion. * `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the collection where the data will be stored. * `@DBRef`: Applied at the field to indicate it is to be stored using a com.mongodb.DBRef. +* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. This can be a single value (the _id_ by default), or a `Document` provided via a converter. * `@Indexed`: Applied at the field level to describe how to index the field. * `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes. * `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field. @@ -826,6 +827,370 @@ Required properties that are also defined as lazy loading ``DBRef`` and used as TIP: Lazily loaded ``DBRef``s can be hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()` or some inline debug rendering invoking property getters. Please consider to enable _trace_ logging for `org.springframework.data.mongodb.core.convert.DefaultDbRefResolver` to gain insight on `DBRef` resolution. +[[mapping-usage.linking]] +=== Using Document References + +Using `@DocumentReference` offers an alternative way of linking entities in MongoDB. +While the goal is the same as when using <>, the store representation is different. +`DBRef` resolves to a document with a fixed structure as outlined in the https://docs.mongodb.com/manual/reference/database-references/[MongoDB Reference documentation]. + +Document references, do not follow a specific format. +They can be literally anything, a single value, an entire document, basically everything that can be stored in MongoDB. +By default, the mapping layer will use the referenced entities _id_ value for storage and retrieval, like in the sample below. + +==== +[source,java] +---- +@Document +public class Account { + + @Id + private String id; + private Float total; +} + +@Document +public class Person { + + @Id + private String id; + + @DocumentReference <1> + private List accounts; +} +---- +[source,java] +---- +Account account = ... + +tempate.insert(account); <2> + +template.update(Person.class) + .matching(where("id").is(...)) + .apply(new Update().push("accounts").value(account)) <3> + .first(); +---- +[source,json] +---- +{ + "_id" : ..., + "accounts" : [ "6509b9e", ... ] <4> +} +---- +<1> Mark the collection of `Account` values to be linked. +<2> The mapping framework does not handle cascading saves, so make sure to persist the referenced entity individually. +<3> Add the reference to the existing entity. +<4> Linked `Account` entities are represented as an array of their `_id` values. +==== + +The sample above uses an `_id` based fetch query (`{ '_id' : ?#{#target} }`) for data retrieval and resolves linked entities eagerly. +It is possible to alter resolution defaults (listed below) via the attributes of `@DocumentReference` + +.@DocumentReference defaults +[cols="2,3,5", options="header"] +|=== +| Attribute | Description | Default + +| `db` +| The target database name for collection lookup. +| The configured database provided by `MongoDatabaseFactory.getMongoDatabase()`. + +| `collection` +| The target collection name. +| The annotated properties domain type, respectively the value type in case of `Collection` like or `Map` properties, collection name. + +| `lookup` +| The single document lookup query evaluating placeholders via SpEL expressions using `#target` as the marker for a given source value. `Collection` like or `Map` properties combine individual lookups via an `$or` operator. +| An `_id` field based query (`{ '_id' : ?#{#target} }`) using the loaded source value. + +| `lazy` +| If set to `true` value resolution is delayed upon first access of the property. +| Resolves properties eagerly by default. +|=== + +`@DocumentReference(lookup=...)` allows to define custom queries that are independent from the `_id` field and therefore offer a flexible way of defining links between entities as demonstrated in the sample below, where the `Publisher` of a book is referenced by its acronym instead of the internal `id`. + +==== +[source,java] +---- +@Document +public class Book { + + @Id + private ObjectId id; + private String title; + private List author; + + @Field("publisher_ac") + @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }") <1> + private Publisher publisher; +} + +@Document +public class Publisher { + + @Id + private ObjectId id; + private String acronym; <1> + private String name; + + @DocumentReference(lazy = true) <2> + private List books; + +} +---- +[source,json] +---- +{ + "_id" : 9a48e32, + "title" : "The Warded Man", + "author" : ["Peter V. Brett"], + "publisher_ac" : "DR" +} +---- +<1> Use the `acronym` field to query for entities in the `Publisher` collection. +<2> Lazy load back references to the `Book` collection. +==== + +The above snipped shows the reading side of things when working with custom linked objects. +To make the writing part aware of the modified document pointer a custom converter, capable of the transformation into a `DocumentPointer`, like the one below, needs to be registered. + +==== +[source,java] +---- +@WritingConverter +class PublisherReferenceConverter implements Converter> { + + @Override + public DocumentPointer convert(Publisher source) { + return () -> source.getAcronym(); + } +} +---- +==== + +If no `DocumentPointer` converter is provided the target linkage document can be computed based on the given lookup query. +In this case the association target properties are evaluated as shown in the following sample. + +==== +[source,java] +---- +@Document +public class Book { + + @Id + private ObjectId id; + private String title; + private List author; + + @DocumentReference(lookup = "{ 'acronym' : ?#{acc} }") <1> <2> + private Publisher publisher; +} + +@Document +public class Publisher { + + @Id + private ObjectId id; + private String acronym; <1> + private String name; + + // ... +} +---- +[source,json] +---- +{ + "_id" : 9a48e32, + "title" : "The Warded Man", + "author" : ["Peter V. Brett"], + "publisher" : { + "acc" : "DOC" + } +} +---- +<1> Use the `acronym` field to query for entities in the `Publisher` collection. +<2> The field value placeholders of the lookup query (like `acc`) is used to form the linkage document. +==== + +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. + +.Simple Document Reference using _id_ field +==== +[source,java] +---- +class Entity { + @DocumentReference + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : "9a48e32" <1> +} + +// referenced object +{ + "_id" : "9a48e32" <1> +} +---- +<1> MongoDB simple type can be directly used without further configuration. +==== + +.Simple Document Reference using _id_ field with explicit lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") <1> + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : "9a48e32" <1> +} + +// referenced object +{ + "_id" : "9a48e32" +} +---- +<1> _target_ defines the linkage value itself. +==== + +.Document Reference extracting field of linkage document for lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{refKey}' }") <1> <2> + private ReferencedObject ref; +} +---- + +[source,java] +---- +@WritingConverter +class ToDocumentPointerConverter implements Converter> { + public DocumentPointer convert(ReferencedObject source) { + return () -> new Document("refKey", source.id); <1> + } +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "refKey" : "9a48e32" <1> + } +} + +// referenced object +{ + "_id" : "9a48e32" +} +---- +<1> The key used for obtaining the linkage value must be the one used during write. +<2> `refKey` is short for `target.refKey`. +==== + +.Document Reference with multiple values forming the lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ 'firstname' : '?#{fn}', 'lastname' : '?#{ln}' }") <1> <2> + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "fn" : "Josh", <1> + "ln" : "Long" <1> + } +} + +// referenced object +{ + "_id" : "9a48e32", + "firsntame" : "Josh", <2> + "lastname" : "Long", <2> +} +---- +<1> Read/wirte the keys `fn` & `ln` from/to the linkage document based on the lookup query. +<2> Use non _id_ fields for the lookup of the target documents. +==== + +.Document Reference reading target collection from linkage document +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") <2> + private ReferencedObject ref; +} +---- + +[source,java] +---- +@WritingConverter +class ToDocumentPointerConverter implements Converter> { + public DocumentPointer convert(ReferencedObject source) { + return () -> new Document("id", source.id) <1> + .append("collection", ... ); <2> + } +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "id" : "9a48e32", <1> + "collection" : "..." <2> + } +} +---- +<1> Read/wirte the keys `_id` from/to the linkage document to use them in the lookup query. +<2> The collection name can be read from the linkage document via its key. +==== + +[WARNING] +==== +We know it is tempting to use all kinds of MongoDB query operators in the lookup query and this is fine. But: + +* Make sure to have indexes in place that support your lookup. +* Mind that resolution takes time and consider a lazy strategy. +* A collection of document references is bulk loaded using an `$or` operator. + +The original element order is restored in memory which cannot be done when using MongoDB query operators. +In this case Results will be ordered as they are received from the store. + +And a few more general remarks: + +* Cyclic references? Ask your self if you need them. +* Lazy document references are hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()`. +* There is no support for reading document references via the reactive bits Spring Data MongoDB offers. +==== + [[mapping-usage-events]] === Mapping Framework Events