From 6ed274bd9bbff846546ebac412cea6be73a2e911 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 5 May 2021 09:44:53 +0200 Subject: [PATCH] Update entity linking support to derive document pointer from lookup query. Simplify usage by computing the pointer from the lookup. Update the reference documentation, add JavaDoc and refine API. Original pull request: #3647. Closes #3602. --- .../core/convert/DefaultDbRefResolver.java | 15 +- .../core/convert/DefaultReferenceLoader.java | 14 +- .../convert/DefaultReferenceResolver.java | 22 +- .../core/convert/DocumentPointerFactory.java | 135 +++++ .../core/convert/LazyLoadingProxy.java | 11 + .../convert/LazyLoadingProxyGenerator.java | 30 +- .../core/convert/MappingMongoConverter.java | 108 +++- .../mongodb/core/convert/MongoWriter.java | 4 + .../core/convert/NoOpDbRefResolver.java | 7 +- .../mongodb/core/convert/QueryMapper.java | 12 +- .../mongodb/core/convert/ReferenceLoader.java | 38 +- .../mongodb/core/convert/ReferenceReader.java | 136 ++--- .../core/convert/ReferenceResolver.java | 45 +- .../mapping/BasicMongoPersistentProperty.java | 19 + ...ectReference.java => DocumentPointer.java} | 13 +- .../core/mapping/DocumentReference.java | 86 ++- .../core/mapping/MongoPersistentProperty.java | 19 + .../UnwrappedMongoPersistentProperty.java | 11 + .../MongoTemplateDocumentReferenceTests.java | 548 ++++++++++++++++-- .../DefaultDbRefResolverUnitTests.java | 7 +- .../core/convert/QueryMapperUnitTests.java | 29 + .../performance/ReactivePerformanceTests.java | 7 +- ...tractPersonRepositoryIntegrationTests.java | 15 + .../data/mongodb/repository/Person.java | 12 + .../mongodb/repository/PersonRepository.java | 2 + src/main/asciidoc/new-features.adoc | 5 + src/main/asciidoc/reference/mapping.adoc | 365 ++++++++++++ 27 files changed, 1468 insertions(+), 247 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/{ObjectReference.java => DocumentPointer.java} (57%) 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