Browse Source

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.
pull/3662/head
Christoph Strobl 5 years ago committed by Mark Paluch
parent
commit
6ed274bd9b
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java
  2. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java
  3. 22
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java
  4. 135
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java
  5. 11
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java
  6. 30
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java
  7. 106
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  8. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java
  9. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java
  10. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  11. 36
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java
  12. 136
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java
  13. 45
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java
  14. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
  15. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentPointer.java
  16. 86
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java
  17. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
  18. 11
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java
  19. 548
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java
  20. 7
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java
  21. 29
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
  22. 7
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java
  23. 15
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  24. 12
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
  25. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  26. 5
      src/main/asciidoc/new-features.adoc
  27. 365
      src/main/asciidoc/reference/mapping.adoc

15
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.LazyLoadingException;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils; 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.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd; import org.springframework.objenesis.ObjenesisStd;
@ -117,7 +117,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
*/ */
@Override @Override
public Document fetch(DBRef dbRef) { 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()); databaseSource.getCollectionName());
} }
List<Document> result = getReferenceLoader() List<Document> result = mongoCollection //
.bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next())) .find(new Document("_id", new Document("$in", ids))) //
.collect(Collectors.toList()); .into(new ArrayList<>());
return ids.stream() // return ids.stream() //
.flatMap(id -> documentWithId(id, result)) // .flatMap(id -> documentWithId(id, result)) //
@ -498,9 +499,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
.getCollection(dbref.getCollectionName(), Document.class); .getCollection(dbref.getCollectionName(), Document.class);
} }
protected MongoCollection<Document> getCollection(ReferenceContext context) { protected MongoCollection<Document> getCollection(ReferenceCollection context) {
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class); Document.class);
} }
} }

14
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; package org.springframework.data.mongodb.core.convert;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.MongoDatabaseUtils;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
/** /**
@ -49,7 +43,7 @@ public class DefaultReferenceLoader implements ReferenceLoader {
} }
@Override @Override
public Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context) { public Iterable<Document> bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context) {
MongoCollection<Document> collection = getCollection(context); MongoCollection<Document> collection = getCollection(context);
@ -63,9 +57,9 @@ public class DefaultReferenceLoader implements ReferenceLoader {
return filter.apply(collection); return filter.apply(collection);
} }
protected MongoCollection<Document> getCollection(ReferenceContext context) { protected MongoCollection<Document> getCollection(ReferenceCollection context) {
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class); Document.class);
} }
} }

22
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; 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.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -44,24 +38,26 @@ public class DefaultReferenceResolver implements ReferenceResolver {
@Nullable @Nullable
@Override @Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {
if (isLazyReference(property)) { 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, private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { ReferenceReader referenceReader, LookupFunction lookupFunction,
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction); ResultConversionFunction resultConversionFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction,
resultConversionFunction);
} }
protected boolean isLazyReference(MongoPersistentProperty property) { protected boolean isLazyReference(MongoPersistentProperty property) {
if (property.findAnnotation(DocumentReference.class) != null) { if (property.isDocumentReference()) {
return property.findAnnotation(DocumentReference.class).lazy(); return property.getDocumentReference().lazy();
} }
return property.getDBRef() != null && property.getDBRef().lazy(); return property.getDBRef() != null && property.getDBRef().lazy();

135
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<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private Map<String, LinkageDocument> linkageMap;
public DocumentPointerFactory(ConversionService conversionService,
MappingContext<? extends MongoPersistentEntity<?>, 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<Integer, String> 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<String, Object> 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;
}
}
}

11
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java

@ -46,4 +46,15 @@ public interface LazyLoadingProxy {
*/ */
@Nullable @Nullable
DBRef toDBRef(); 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();
}
} }

30
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.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ProxyFactory;
import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.objenesis.ObjenesisStd; import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -54,11 +50,12 @@ class LazyLoadingProxyGenerator {
this.objenesis = new ObjenesisStd(true); this.objenesis = new ObjenesisStd(true);
} }
public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { ResultConversionFunction resultConversionFunction) {
Class<?> propertyType = property.getType(); Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction); LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction,
resultConversionFunction);
if (!propertyType.isInterface()) { if (!propertyType.isInterface()) {
@ -105,27 +102,30 @@ class LazyLoadingProxyGenerator {
private volatile boolean resolved; private volatile boolean resolved;
private @org.springframework.lang.Nullable Object result; private @org.springframework.lang.Nullable Object result;
private Object source; private Object source;
private BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> 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 { try {
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget"); INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef"); TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize"); FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
GET_SOURCE_METHOD = LazyLoadingProxy.class.getMethod("getSource");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader, public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {
this.property = property; this.property = property;
this.source = source; this.source = source;
this.referenceReader = reader; this.referenceReader = reader;
this.lookupFunction = lookupFunction; this.lookupFunction = lookupFunction;
this.resultConversionFunction = resultConversionFunction;
} }
@Nullable @Nullable
@ -145,6 +145,10 @@ class LazyLoadingProxyGenerator {
return null; return null;
} }
if (GET_SOURCE_METHOD.equals(method)) {
return source;
}
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) { if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
if (ReflectionUtils.isToStringMethod(method)) { if (ReflectionUtils.isToStringMethod(method)) {
@ -234,7 +238,7 @@ class LazyLoadingProxyGenerator {
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()); // 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) { } catch (RuntimeException ex) {
throw ex; throw ex;

106
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.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoDatabaseFactory; 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.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; 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;
import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty; import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
@ -124,6 +123,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private SpELContext spELContext; private SpELContext spELContext;
private @Nullable EntityCallbacks entityCallbacks; private @Nullable EntityCallbacks entityCallbacks;
private DocumentPointerFactory documentPointerFactory;
/** /**
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. * 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); return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator);
}); });
this.referenceReader = new ReferenceReader(mappingContext, this.referenceReader = new ReferenceReader(mappingContext, () -> spELContext);
(prop, document) -> this.read(prop.getActualType(), document), () -> spELContext); this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext);
} }
/** /**
@ -366,6 +366,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
DocumentAccessor documentAccessor = new DocumentAccessor(bson); 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<S, MongoPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor(); PreferredConstructor<S, MongoPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null
@ -376,6 +384,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
S instance = instantiator.createInstance(entity, provider); S instance = instantiator.createInstance(entity, provider);
if (entity.requiresPropertyPopulation()) { if (entity.requiresPropertyPopulation()) {
return populateProperties(context, entity, documentAccessor, evaluator, instance); return populateProperties(context, entity, documentAccessor, evaluator, instance);
} }
@ -451,7 +460,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
callback = getDbRefResolverCallback(context, documentAccessor, evaluator); callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
} }
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
evaluator);
continue; continue;
} }
@ -478,7 +488,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
callback = getDbRefResolverCallback(context, documentAccessor, evaluator); callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
} }
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
evaluator);
continue; continue;
} }
@ -494,7 +505,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
private void readAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor<?> accessor, private void readAssociation(Association<MongoPersistentProperty> association, PersistentPropertyAccessor<?> accessor,
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) { DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback,
ConversionContext context, SpELExpressionEvaluator evaluator) {
MongoPersistentProperty property = association.getInverse(); MongoPersistentProperty property = association.getInverse();
final Object value = documentAccessor.get(property); final Object value = documentAccessor.get(property);
@ -503,26 +515,32 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return; return;
} }
if (property.isAnnotationPresent(DocumentReference.class)) { if (property.isDocumentReference()) {
// quite unusual but sounds like worth having? // 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 DocumentPointer<?> pointer = new DocumentPointer<Object>() {
accessor.setProperty(property, conversionService.convert(new ObjectReference() {
@Override @Override
public Object getPointer() { public Object getPointer() {
return value; return value;
} }
}, property.getActualType())); };
// collection like special treatment
accessor.setProperty(property, conversionService.convert(pointer, property.getActualType()));
} else { } else {
accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader)); accessor.setProperty(property,
dbRefResolver.resolveReference(property, value, referenceReader, context::convert));
} }
return; return;
} }
DBRef dbref = value instanceof DBRef ? (DBRef) value : null; 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)); accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
} }
@ -563,6 +581,45 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return createDBRef(object, referringProperty); 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 * Root entry method into write conversion. Adds a type discriminator to the {@link Document}. Shouldn't be called for
* nested conversions. * nested conversions.
@ -749,13 +806,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (prop.isAssociation()) { if (prop.isAssociation()) {
if (conversionService.canConvert(valueType.getType(), ObjectReference.class)) { accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext)
accessor.put(prop, conversionService.convert(obj, ObjectReference.class).getPointer()); .computePointer(prop, obj, valueType.getType()).getPointer());
} else {
// just take the id as a reference
accessor.put(prop, mappingContext.getPersistentEntity(prop.getAssociationTargetType())
.getIdentifierAccessor(obj).getIdentifier());
}
return; return;
} }
@ -799,14 +851,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (property.isAssociation()) { if (property.isAssociation()) {
return writeCollectionInternal(collection.stream().map(it -> { return writeCollectionInternal(collection.stream().map(it -> {
if (conversionService.canConvert(it.getClass(), ObjectReference.class)) { if (conversionService.canConvert(it.getClass(), DocumentPointer.class)) {
return conversionService.convert(it, ObjectReference.class).getPointer(); return conversionService.convert(it, DocumentPointer.class).getPointer();
} else { } else {
// just take the id as a reference // just take the id as a reference
return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it) return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it)
.getIdentifier(); .getIdentifier();
} }
}).collect(Collectors.toList()), ClassTypeInformation.from(ObjectReference.class), new BasicDBList()); }).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new BasicDBList());
} }
if (property.hasExplicitWriteTarget()) { if (property.hasExplicitWriteTarget()) {
@ -858,12 +910,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (property.isDbReference()) { if (property.isDbReference()) {
document.put(simpleKey, value != null ? createDBRef(value, property) : null); document.put(simpleKey, value != null ? createDBRef(value, property) : null);
} else { } else {
if (conversionService.canConvert(value.getClass(), ObjectReference.class)) { if (conversionService.canConvert(value.getClass(), DocumentPointer.class)) {
document.put(simpleKey, conversionService.convert(value, ObjectReference.class).getPointer()); document.put(simpleKey, conversionService.convert(value, DocumentPointer.class).getPointer());
} else { } else {
// just take the id as a reference // just take the id as a reference
document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value) document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType())
.getIdentifier()); .getIdentifierAccessor(value).getIdentifier());
} }
} }

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java

@ -70,4 +70,8 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
* @return will never be {@literal null}. * @return will never be {@literal null}.
*/ */
DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty); DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty);
default Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) {
return toDBRef(source, referringProperty);
}
} }

7
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 java.util.stream.Stream;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import com.mongodb.DBRef; import com.mongodb.DBRef;
@ -77,7 +77,8 @@ public enum NoOpDbRefResolver implements DbRefResolver {
@Nullable @Nullable
@Override @Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { LookupFunction lookupFunction,
ResultConversionFunction resultConversionFunction) {
return null; return null;
} }

12
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) { if (source instanceof Iterable) {
BasicDBList result = new BasicDBList(); BasicDBList result = new BasicDBList();
for (Object element : (Iterable<?>) source) { for (Object element : (Iterable<?>) source) {
result.add(createDbRefFor(element, property)); result.add(createReferenceFor(element, property));
} }
return result; return result;
} }
@ -614,12 +614,12 @@ public class QueryMapper {
Document result = new Document(); Document result = new Document();
Document dbObject = (Document) source; Document dbObject = (Document) source;
for (String key : dbObject.keySet()) { for (String key : dbObject.keySet()) {
result.put(key, createDbRefFor(dbObject.get(key), property)); result.put(key, createReferenceFor(dbObject.get(key), property));
} }
return result; 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(); 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) { if (source instanceof DBRef) {
return (DBRef) source; return (DBRef) source;
} }
if(property != null && property.isDocumentReference()) {
return converter.toDocumentReference(source, property);
}
return converter.toDBRef(source, property); return converter.toDBRef(source, property);
} }

36
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; package org.springframework.data.mongodb.core.convert;
import java.util.stream.Stream; import java.util.Collections;
import java.util.stream.StreamSupport; import java.util.Iterator;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; 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 org.springframework.lang.Nullable;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
@ -31,15 +31,15 @@ import com.mongodb.client.MongoCollection;
public interface ReferenceLoader { public interface ReferenceLoader {
@Nullable @Nullable
default Document fetch(ReferenceFilter filter, ReferenceContext context) { default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) {
return bulkFetch(filter, context).findFirst().orElse(null);
Iterator<Document> it = bulkFetch(filter, context).iterator();
return it.hasNext() ? it.next() : null;
} }
// meh, Stream! Iterable<Document> bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context);
Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context);
// Reference query interface DocumentReferenceQuery {
interface ReferenceFilter {
Bson getFilter(); 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 // TODO: Move apply method into something else that holds the collection and knows about single item/multi-item
// processing // processing
default Stream<Document> apply(MongoCollection<Document> collection) { default Iterable<Document> apply(MongoCollection<Document> collection) {
return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false)); return restoreOrder(collection.find(getFilter()).sort(getSort()));
} }
default Stream<Document> restoreOrder(Stream<Document> stream) { default Iterable<Document> restoreOrder(Iterable<Document> documents) {
return stream; return documents;
} }
static ReferenceFilter referenceFilter(Bson bson) { static DocumentReferenceQuery referenceFilter(Bson bson) {
return () -> bson; return () -> bson;
} }
static ReferenceFilter singleReferenceFilter(Bson bson) { static DocumentReferenceQuery singleReferenceFilter(Bson bson) {
return new ReferenceFilter() { return new DocumentReferenceQuery() {
@Override @Override
public Bson getFilter() { public Bson getFilter() {
@ -71,10 +71,10 @@ public interface ReferenceLoader {
} }
@Override @Override
public Stream<Document> apply(MongoCollection<Document> collection) { public Iterable<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getFilter()).sort(getSort()).limit(1).first(); 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();
} }
}; };
} }

136
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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mapping.model.SpELContext;
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.ReferenceResolver.ReferenceContext; 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.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@ -56,61 +56,47 @@ import com.mongodb.client.MongoCollection;
*/ */
public class ReferenceReader { public class ReferenceReader {
private final ParameterBindingDocumentCodec codec;
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext; private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
private final BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction;
private final Supplier<SpELContext> spelContextSupplier; private final Supplier<SpELContext> spelContextSupplier;
private final ParameterBindingDocumentCodec codec;
public ReferenceReader(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, public ReferenceReader(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
Supplier<SpELContext> spelContextSupplier) { Supplier<SpELContext> spelContextSupplier) {
this(() -> mappingContext, documentConversionFunction, spelContextSupplier); this(() -> mappingContext, spelContextSupplier);
} }
public ReferenceReader( public ReferenceReader(
Supplier<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContextSupplier, Supplier<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContextSupplier,
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
Supplier<SpELContext> spelContextSupplier) { Supplier<SpELContext> spelContextSupplier) {
this.mappingContext = Lazy.of(mappingContextSupplier); this.mappingContext = Lazy.of(mappingContextSupplier);
this.documentConversionFunction = documentConversionFunction;
this.spelContextSupplier = spelContextSupplier; this.spelContextSupplier = spelContextSupplier;
this.codec = new ParameterBindingDocumentCodec(); this.codec = new ParameterBindingDocumentCodec();
} }
// TODO: Move documentConversionFunction to here. Having a contextual read allows projections in references Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction,
Object readReference(MongoPersistentProperty property, Object value, ResultConversionFunction resultConversionFunction) {
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
SpELContext spELContext = spelContextSupplier.get(); SpELContext spELContext = spelContextSupplier.get();
ReferenceFilter filter = computeFilter(property, value, spELContext); DocumentReferenceQuery filter = computeFilter(property, value, spELContext);
ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext); ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext);
Stream<Document> result = lookupFunction.apply(referenceContext, filter); Iterable<Document> result = lookupFunction.apply(filter, referenceCollection);
if (property.isCollectionLike()) { if (!result.iterator().hasNext()) {
return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList()); return null;
} }
// TODO: retain target type and extract types here so the conversion function doesn't require type fiddling if (property.isCollectionLike()) {
// BiFunction<TypeInformation, Document, Object> instead of MongoPersistentProperty return resultConversionFunction.apply(result, property.getTypeInformation());
if (property.isMap()) {
// the order is a real problem here
Iterator<Object> 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);
} }
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) { SpELContext spELContext) {
if (value instanceof Iterable) { if (value instanceof Iterable) {
@ -118,44 +104,43 @@ public class ReferenceReader {
} }
if (value instanceof DBRef) { if (value instanceof DBRef) {
return ReferenceContext.fromDBRef((DBRef) value); return ReferenceCollection.fromDBRef((DBRef) value);
} }
if (value instanceof Document) { if (value instanceof Document) {
Document ref = (Document) value; Document ref = (Document) value;
if (property.isAnnotationPresent(DocumentReference.class)) { if (property.isDocumentReference()) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); DocumentReference documentReference = property.getDocumentReference();
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext,
() -> ref.get("db", String.class)); () -> ref.get("db", String.class));
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
() -> ref.get("collection", () -> ref.get("collection",
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); 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())); mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
} }
if (property.isAnnotationPresent(DocumentReference.class)) { if (property.isDocumentReference()) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); DocumentReference documentReference = property.getDocumentReference();
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null);
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
() -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); () -> 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()); mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
} }
@ -201,9 +186,9 @@ public class ReferenceReader {
return ctx; 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(); String lookup = documentReference.lookup();
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null); Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null);
@ -217,7 +202,7 @@ public class ReferenceReader {
ors.add(decoded); 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) { if (property.isMap() && value instanceof Map) {
@ -230,18 +215,18 @@ public class ReferenceReader {
filterMap.put(entry.getKey(), decoded); 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 filter;
Document sort; Document sort;
public SingleReferenceFilter(Document filter, Document sort) { public SingleDocumentReferenceQuery(Document filter, Document sort) {
this.filter = filter; this.filter = filter;
this.sort = sort; this.sort = sort;
} }
@ -252,24 +237,24 @@ public class ReferenceReader {
} }
@Override @Override
public Stream<Document> apply(MongoCollection<Document> collection) { public Iterable<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getFilter()).limit(1).first(); 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; private final Document filter;
Document sort; private final Document sort;
Map<Object, Document> filterOrderMap; private final Map<Object, Document> filterOrderMap;
public MapReferenceFilter(Document filter, Document sort, Map<Object, Document> filterOrderMap) { public MapDocumentReferenceQuery(Document filter, Document sort, Map<Object, Document> filterOrderMap) {
this.filter = filter; this.filter = filter;
this.filterOrderMap = filterOrderMap;
this.sort = sort; this.sort = sort;
this.filterOrderMap = filterOrderMap;
} }
@Override @Override
@ -283,45 +268,46 @@ public class ReferenceReader {
} }
@Override @Override
public Stream<Document> restoreOrder(Stream<Document> stream) { public Iterable<Document> restoreOrder(Iterable<Document> documents) {
Map<String, Object> targetMap = new LinkedHashMap<>(); Map<String, Object> targetMap = new LinkedHashMap<>();
List<Document> collected = stream.collect(Collectors.toList()); List<Document> collected = documents instanceof List ? (List<Document>) documents
: Streamable.of(documents).toList();
for (Entry<Object, Document> filterMapping : filterOrderMap.entrySet()) { for (Entry<Object, Document> filterMapping : filterOrderMap.entrySet()) {
String key = filterMapping.getKey().toString(); Optional<Document> first = collected.stream()
Optional<Document> first = collected.stream().filter(it -> { .filter(it -> it.entrySet().containsAll(filterMapping.getValue().entrySet())).findFirst();
boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet()); targetMap.put(filterMapping.getKey().toString(), first.orElse(null));
return found;
}).findFirst();
targetMap.put(key, 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; private final Document filter;
Document sort; private final Document sort;
public ListDocumentReferenceQuery(Document filter, Document sort) {
public ListReferenceFilter(Document filter, Document sort) {
this.filter = filter; this.filter = filter;
this.sort = sort; this.sort = sort;
} }
@Override @Override
public Stream<Document> restoreOrder(Stream<Document> stream) { public Iterable<Document> restoreOrder(Iterable<Document> documents) {
if (filter.containsKey("$or")) { if (filter.containsKey("$or")) {
List<Document> ors = filter.get("$or", List.class); List<Document> ors = filter.get("$or", List.class);
return stream.sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)); List<Document> target = documents instanceof List ? (List<Document>) 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() { public Document getFilter() {
@ -347,7 +333,5 @@ public class ReferenceReader {
} }
return referenceList.size(); return referenceList.size();
} }
} }
} }

45
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; package org.springframework.data.mongodb.core.convert;
import java.util.function.BiFunction; import java.util.Collections;
import java.util.stream.Stream;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import com.mongodb.DBRef; import com.mongodb.DBRef;
@ -33,34 +32,38 @@ public interface ReferenceResolver {
@Nullable @Nullable
Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction); LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction);
default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader) { default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
return resolveReference(property, source, referenceReader, (ctx, filter) -> { ResultConversionFunction resultConversionFunction) {
return resolveReference(property, source, referenceReader, (filter, ctx) -> {
if (property.isCollectionLike() || property.isMap()) { if (property.isCollectionLike() || property.isMap()) {
return getReferenceLoader().bulkFetch(filter, ctx); return getReferenceLoader().bulkFetch(filter, ctx);
} }
Object target = getReferenceLoader().fetch(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(); ReferenceLoader getReferenceLoader();
// TODO: ReferenceCollection class ReferenceCollection {
class ReferenceContext {
@Nullable final String database; @Nullable
final String collection; 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.database = database;
this.collection = collection; this.collection = collection;
} }
static ReferenceContext fromDBRef(DBRef dbRef) { static ReferenceCollection fromDBRef(DBRef dbRef) {
return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName()); return new ReferenceCollection(dbRef.getDatabaseName(), dbRef.getCollectionName());
} }
public String getCollection() { public String getCollection() {
@ -72,4 +75,14 @@ public interface ReferenceResolver {
return database; return database;
} }
} }
@FunctionalInterface
interface LookupFunction {
Iterable<Document> apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection);
}
@FunctionalInterface
interface ResultConversionFunction {
Object apply(Object source, TypeInformation property);
}
} }

19
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); 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) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef() * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef()
@ -240,6 +249,16 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
return findAnnotation(DBRef.class); 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) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty() * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty()

13
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java → 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; 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 * @author Christoph Strobl
*/ */
@FunctionalInterface @FunctionalInterface
// TODO: ObjectPointer or DocumentPointer public interface DocumentPointer<T> {
public interface ObjectReference<T> {
/**
* 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(); T getPointer();
} }

86
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; 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.
*
* <pre class="code">
* public class Account {
* private String id;
* private Float total;
* }
*
* public class Person {
* private String id;
* &#64;DocumentReference
* private List&lt;Account&gt; accounts;
* }
*
* Account account = ...
*
* mongoTemplate.insert(account);
*
* template.update(Person.class)
* .matching(where("id").is(...))
* .apply(new Update().push("accounts").value(account))
* .first();
* </pre>
*
* {@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.
*
* <pre class="code">
* public class Book {
* private ObjectId id;
* private String title;
*
* &#64;Field("publisher_ac")
* &#64;DocumentReference(lookup = "{ 'acronym' : ?#{#target} }")
* private Publisher publisher;
* }
*
* public class Publisher {
*
* private ObjectId id;
* private String acronym;
* private String name;
*
* &#64;DocumentReference(lazy = true)
* private List&lt;Book&gt; books;
* }
*
* &#64;WritingConverter
* public class PublisherReferenceConverter implements Converter&lt;Publisher, DocumentPointer&lt;String&gt;&gt; {
*
* public DocumentPointer&lt;String&gt; convert(Publisher source) {
* return () -> source.getAcronym();
* }
* }
* </pre>
*
* @author Christoph Strobl * @author Christoph Strobl
* @since 3.3 * @since 3.3
* @see <a href="https://docs.mongodb.com/manual/reference/database-references/#std-label-document-references">MongoDB Reference Documentation</a>
*/ */
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -34,17 +95,38 @@ import org.springframework.data.annotation.Reference;
public @interface DocumentReference { 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 ""; 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 ""; 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} }"; String lookup() default "{ '_id' : ?#{#target} }";
/**
* A specific sort.
*
* @return empty String by default.
*/
String sort() 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; boolean lazy() default false;
} }

19
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java

@ -62,6 +62,15 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
*/ */
boolean isDbReference(); boolean isDbReference();
/**
* Returns whether the property is a {@link DocumentReference}. If this returns {@literal true} you can expect
* {@link #getDocumentReference()} to return an non-{@literal null} value.
*
* @return
* @since 3.3
*/
boolean isDocumentReference();
/** /**
* Returns whether the property is explicitly marked as an identifier property of the owning {@link PersistentEntity}. * Returns whether the property is explicitly marked as an identifier property of the owning {@link PersistentEntity}.
* A property is an explicit id property if it is annotated with @see {@link Id}. * A property is an explicit id property if it is annotated with @see {@link Id}.
@ -105,6 +114,16 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
@Nullable @Nullable
DBRef getDBRef(); DBRef getDBRef();
/**
* Returns the {@link DocumentReference} if the property is a reference.
*
* @see #isDocumentReference()
* @return {@literal null} if not present.
* @since 3.3
*/
@Nullable
DocumentReference getDocumentReference();
/** /**
* Returns whether property access shall be used for reading the property value. This means it will use the getter * Returns whether property access shall be used for reading the property value. This means it will use the getter
* instead of field access. * instead of field access.

11
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java

@ -68,6 +68,11 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return delegate.isDbReference(); return delegate.isDbReference();
} }
@Override
public boolean isDocumentReference() {
return delegate.isDocumentReference();
}
@Override @Override
public boolean isExplicitIdProperty() { public boolean isExplicitIdProperty() {
return delegate.isExplicitIdProperty(); return delegate.isExplicitIdProperty();
@ -94,6 +99,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return delegate.getDBRef(); return delegate.getDBRef();
} }
@Override
@Nullable
public DocumentReference getDocumentReference() {
return delegate.getDocumentReference();
}
@Override @Override
public boolean usePropertyAccess() { public boolean usePropertyAccess() {
return delegate.usePropertyAccess(); return delegate.usePropertyAccess();

548
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java

@ -23,6 +23,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -32,16 +33,17 @@ import java.util.Map;
import org.bson.Document; import org.bson.Document;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils; import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils;
import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.ObjectReference; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestTemplate;
@ -51,14 +53,14 @@ import com.mongodb.client.MongoClient;
import com.mongodb.client.model.Filters; import com.mongodb.client.model.Filters;
/** /**
* {@link DBRef} related integration tests for {@link MongoTemplate}. * {@link DocumentReference} related integration tests for {@link MongoTemplate}.
* *
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@ExtendWith(MongoClientExtension.class) @ExtendWith(MongoClientExtension.class)
public class MongoTemplateDocumentReferenceTests { public class MongoTemplateDocumentReferenceTests {
public static final String DB_NAME = "manual-reference-tests"; public static final String DB_NAME = "document-reference-tests";
static @Client MongoClient client; static @Client MongoClient client;
@ -71,7 +73,8 @@ public class MongoTemplateDocumentReferenceTests {
}); });
cfg.configureConversion(it -> { cfg.configureConversion(it -> {
it.customConverters(new ReferencableConverter()); it.customConverters(new ReferencableConverter(), new SimpleObjectRefWithReadingConverterToDocumentConverter(),
new DocumentToSimpleObjectRefWithReadingConverter());
}); });
cfg.configureMappingContext(it -> { cfg.configureMappingContext(it -> {
@ -84,7 +87,7 @@ public class MongoTemplateDocumentReferenceTests {
template.flushDatabase(); template.flushDatabase();
} }
@Test @Test // GH-3602
void writeSimpleTypeReference() { void writeSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -102,12 +105,11 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("simpleValueRef")).isEqualTo("ref-1"); assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
} }
@Test @Test // GH-3602
void writeMapTypeReference() { void writeMapTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
CollectionRefRoot source = new CollectionRefRoot(); CollectionRefRoot source = new CollectionRefRoot();
source.id = "root-1"; source.id = "root-1";
source.mapValueRef = new LinkedHashMap<>(); source.mapValueRef = new LinkedHashMap<>();
@ -120,11 +122,10 @@ public class MongoTemplateDocumentReferenceTests {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); 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"); assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2");
} }
@Test @Test // GH-3602
void writeCollectionOfSimpleTypeReference() { void writeCollectionOfSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -143,7 +144,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2"); assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
} }
@Test @Test // GH-3602
void writeObjectTypeReference() { void writeObjectTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -161,7 +162,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference()); assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
} }
@Test @Test // GH-3602
void writeCollectionOfObjectTypeReference() { void writeCollectionOfObjectTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -181,7 +182,7 @@ public class MongoTemplateDocumentReferenceTests {
source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference()); source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
} }
@Test @Test // GH-3602
void readSimpleTypeObjectReference() { void readSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); 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")); assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readCollectionOfSimpleTypeObjectReference() { void readCollectionOfSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); 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")); assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readLazySimpleTypeObjectReference() { void readLazySimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); 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")); assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readSimpleTypeObjectReferenceFromFieldWithCustomName() { void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -266,7 +267,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); .isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readCollectionTypeObjectReferenceFromFieldWithCustomName() { void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -287,7 +288,7 @@ public class MongoTemplateDocumentReferenceTests {
.containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object")); .containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readObjectReferenceFromDocumentType() { void readObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); 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")); assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readCollectionObjectReferenceFromDocumentType() { void readCollectionObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -328,7 +329,7 @@ public class MongoTemplateDocumentReferenceTests {
.containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object")); .containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readObjectReferenceFromDocumentDeclaringCollectionName() { void readObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -351,7 +352,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object")); .isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
} }
@Test @Test // GH-3602
void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() { void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -379,7 +380,7 @@ public class MongoTemplateDocumentReferenceTests {
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object")); new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
} }
@Test @Test // GH-3602
void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() { void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); 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")); .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
} }
@Test @Test // GH-3602
void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() { void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class); 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")); .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
} }
@Test @Test // GH-3602
void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() { void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); 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")); .containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
} }
@Test @Test // GH-3602
void readMapOfReferences() { void readMapOfReferences() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); 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); CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
System.out.println("result: " + result);
assertThat(result.getMapValueRef()).containsEntry("frodo", assertThat(result.getMapValueRef())
new SimpleObjectRef("ref-1", "me-the-1-referenced-object")) .containsEntry("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
.containsEntry("bilbo", .containsEntry("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
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<CollectionRefRoot> 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 @Data
@ -556,16 +959,16 @@ public class MongoTemplateDocumentReferenceTests {
@Id String id; @Id String id;
String value; String value;
} }
@Getter @Getter
@Setter @Setter
static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef { static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) { public SimpleObjectRefWithReadingConverter(String id, String value) {
super(id, value); super(id, value);
} }
} }
@Data @Data
@ -609,41 +1012,94 @@ public class MongoTemplateDocumentReferenceTests {
} }
} }
static class ReferencableConverter implements Converter<ReferenceAble, ObjectReference> { static class ReferencableConverter implements Converter<ReferenceAble, DocumentPointer> {
@Nullable @Nullable
@Override @Override
public ObjectReference convert(ReferenceAble source) { public DocumentPointer convert(ReferenceAble source) {
return source::toReference; return source::toReference;
} }
} }
@WritingConverter @WritingConverter
class DocumentToSimpleObjectRefWithReadingConverter class DocumentToSimpleObjectRefWithReadingConverter
implements Converter<ObjectReference<Document>, SimpleObjectRefWithReadingConverter> { implements Converter<DocumentPointer<Document>, SimpleObjectRefWithReadingConverter> {
private final MongoTemplate template; @Nullable
@Override
public SimpleObjectRefWithReadingConverter convert(DocumentPointer<Document> source) {
public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) { Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref")
this.template = template; .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<SimpleObjectRefWithReadingConverter, DocumentPointer<Document>> {
@Nullable @Nullable
@Override @Override
public SimpleObjectRefWithReadingConverter convert(ObjectReference<Document> source) { public DocumentPointer<Document> convert(SimpleObjectRefWithReadingConverter source) {
return template.findOne(query(where("id").is(source.getPointer().get("the-ref-key-you-did-not-expect"))), return () -> new Document("ref-key-from-custom-write-converter", source.getId());
SimpleObjectRefWithReadingConverter.class);
} }
} }
@WritingConverter @Getter
class SimpleObjectRefWithReadingConverterToDocumentConverter @Setter
implements Converter<SimpleObjectRefWithReadingConverter, ObjectReference<Document>> { 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<ReferencedObject, DocumentPointer<Document>> {
@Nullable @Nullable
@Override @Override
public ObjectReference<Document> convert(SimpleObjectRefWithReadingConverter source) { public DocumentPointer<Document> convert(ReferencedObject source) {
return () -> new Document("the-ref-key-you-did-not-expect", source.getId()); 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;
} }
} }

7
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.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.DocumentTestUtils;
@ -64,8 +65,6 @@ class DefaultDbRefResolverUnitTests {
when(factoryMock.getMongoDatabase()).thenReturn(dbMock); when(factoryMock.getMongoDatabase()).thenReturn(dbMock);
when(dbMock.getCollection(anyString(), any(Class.class))).thenReturn(collectionMock); when(dbMock.getCollection(anyString(), any(Class.class))).thenReturn(collectionMock);
when(collectionMock.find(any(Document.class))).thenReturn(cursorMock); when(collectionMock.find(any(Document.class))).thenReturn(cursorMock);
when(cursorMock.sort(any(Document.class))).thenReturn(cursorMock);
when(cursorMock.spliterator()).thenReturn(Collections.<Document> emptyList().spliterator());
resolver = new DefaultDbRefResolver(factoryMock); resolver = new DefaultDbRefResolver(factoryMock);
} }
@ -116,7 +115,7 @@ class DefaultDbRefResolverUnitTests {
DBRef ref1 = new DBRef("collection-1", o1.get("_id")); DBRef ref1 = new DBRef("collection-1", o1.get("_id"));
DBRef ref2 = new DBRef("collection-1", o2.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); 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 ref1 = new DBRef("collection-1", document.get("_id"));
DBRef ref2 = 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); assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document);
} }

29
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.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document; 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.Field;
import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@ -1487,4 +1488,32 @@ public class QueryMapperUnitTests {
@Field("renamed") @Field("renamed")
String renamed_fieldname_with_underscores; 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());
}
} }

7
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.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*; 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;
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.mongodb.core.convert.ReferenceReader;
import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -107,7 +106,7 @@ public class ReactivePerformanceTests {
@Nullable @Nullable
@Override @Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) { public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {
return null; return null;
} }

15
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()); Person target = repository.findWithAggregationInProjection(alicia.getId());
assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase()); 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<Person> result = repository.findBySpiritAnimal(dave);
assertThat(result).map(Person::getId).containsExactly(josh.getId());
}
} }

12
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.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document; 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.Field;
import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.mongodb.core.mapping.Unwrapped;
@ -74,6 +75,9 @@ public class Person extends Contact {
@Unwrapped.Nullable(prefix = "u") // @Unwrapped.Nullable(prefix = "u") //
User unwrappedUser; User unwrappedUser;
@DocumentReference
User spiritAnimal;
public Person() { public Person() {
this(null, null); this(null, null);
@ -308,6 +312,14 @@ public class Person extends Contact {
this.unwrappedUser = unwrappedUser; this.unwrappedUser = unwrappedUser;
} }
public User getSpiritAnimal() {
return spiritAnimal;
}
public void setSpiritAnimal(User spiritAnimal) {
this.spiritAnimal = spiritAnimal;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* *

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

@ -416,4 +416,6 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
List<Person> findByUnwrappedUserUsername(String username); List<Person> findByUnwrappedUserUsername(String username);
List<Person> findByUnwrappedUser(User user); List<Person> findByUnwrappedUser(User user);
List<Person> findBySpiritAnimal(User user);
} }

5
src/main/asciidoc/new-features.adoc

@ -1,6 +1,11 @@
[[new-features]] [[new-features]]
= New & Noteworthy = New & Noteworthy
[[new-features.3.3]]
== What's New in Spring Data MongoDB 3.3
* Extended support for <<mapping-usage.linking, linking>> entities.
[[new-features.3.2]] [[new-features.3.2]]
== What's New in Spring Data MongoDB 3.2 == What's New in Spring Data MongoDB 3.2

365
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. * `@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. * `@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. * `@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. * `@Indexed`: Applied at the field level to describe how to index the field.
* `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes. * `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes.
* `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field. * `@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. 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. 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 <<mapping-usage-references,DBRefs>>, 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<Account> 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<String> 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<Book> 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<Publisher, DocumentPointer<String>> {
@Override
public DocumentPointer<String> 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<String> 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<ReferencedObject, DocumentPointer<Document>> {
public DocumentPointer<Document> 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<ReferencedObject, DocumentPointer<Document>> {
public DocumentPointer<Document> 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-usage-events]]
=== Mapping Framework Events === Mapping Framework Events

Loading…
Cancel
Save