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. 108
      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; @@ -46,7 +46,7 @@ import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
@ -117,7 +117,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db @@ -117,7 +117,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
*/
@Override
public Document fetch(DBRef dbRef) {
return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef));
return getReferenceLoader().fetch(DocumentReferenceQuery.singleReferenceFilter(Filters.eq("_id", dbRef.getId())),
ReferenceCollection.fromDBRef(dbRef));
}
/*
@ -157,9 +158,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db @@ -157,9 +158,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
databaseSource.getCollectionName());
}
List<Document> result = getReferenceLoader()
.bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next()))
.collect(Collectors.toList());
List<Document> result = mongoCollection //
.find(new Document("_id", new Document("$in", ids))) //
.into(new ArrayList<>());
return ids.stream() //
.flatMap(id -> documentWithId(id, result)) //
@ -498,9 +499,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db @@ -498,9 +499,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
.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);
}
}

14
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java

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

22
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java

@ -15,12 +15,6 @@ @@ -15,12 +15,6 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
@ -44,24 +38,26 @@ public class DefaultReferenceResolver implements ReferenceResolver { @@ -44,24 +38,26 @@ public class DefaultReferenceResolver implements ReferenceResolver {
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {
if (isLazyReference(property)) {
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction);
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction, resultConversionFunction);
}
return referenceReader.readReference(property, source, lookupFunction);
return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction);
}
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction);
ReferenceReader referenceReader, LookupFunction lookupFunction,
ResultConversionFunction resultConversionFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction,
resultConversionFunction);
}
protected boolean isLazyReference(MongoPersistentProperty property) {
if (property.findAnnotation(DocumentReference.class) != null) {
return property.findAnnotation(DocumentReference.class).lazy();
if (property.isDocumentReference()) {
return property.getDocumentReference().lazy();
}
return property.getDBRef() != null && property.getDBRef().lazy();

135
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java

@ -0,0 +1,135 @@ @@ -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 { @@ -46,4 +46,15 @@ public interface LazyLoadingProxy {
*/
@Nullable
DBRef toDBRef();
/**
* Returns the raw {@literal source} object that defines the reference.
*
* @return can be {@literal null}.
* @since 3.3
*/
@Nullable
default Object getSource() {
return toDBRef();
}
}

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

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

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

7
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java

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

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -605,7 +605,7 @@ public class QueryMapper { @@ -605,7 +605,7 @@ public class QueryMapper {
if (source instanceof Iterable) {
BasicDBList result = new BasicDBList();
for (Object element : (Iterable<?>) source) {
result.add(createDbRefFor(element, property));
result.add(createReferenceFor(element, property));
}
return result;
}
@ -614,12 +614,12 @@ public class QueryMapper { @@ -614,12 +614,12 @@ public class QueryMapper {
Document result = new Document();
Document dbObject = (Document) source;
for (String key : dbObject.keySet()) {
result.put(key, createDbRefFor(dbObject.get(key), property));
result.put(key, createReferenceFor(dbObject.get(key), property));
}
return result;
}
return createDbRefFor(source, property);
return createReferenceFor(source, property);
}
/**
@ -666,12 +666,16 @@ public class QueryMapper { @@ -666,12 +666,16 @@ public class QueryMapper {
return Collections.singletonMap(key, value).entrySet().iterator().next();
}
private DBRef createDbRefFor(Object source, MongoPersistentProperty property) {
private Object createReferenceFor(Object source, MongoPersistentProperty property) {
if (source instanceof DBRef) {
return (DBRef) source;
}
if(property != null && property.isDocumentReference()) {
return converter.toDocumentReference(source, property);
}
return converter.toDBRef(source, property);
}

36
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java

@ -15,12 +15,12 @@ @@ -15,12 +15,12 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.Collections;
import java.util.Iterator;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoCollection;
@ -31,15 +31,15 @@ import com.mongodb.client.MongoCollection; @@ -31,15 +31,15 @@ import com.mongodb.client.MongoCollection;
public interface ReferenceLoader {
@Nullable
default Document fetch(ReferenceFilter filter, ReferenceContext context) {
return bulkFetch(filter, context).findFirst().orElse(null);
default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) {
Iterator<Document> it = bulkFetch(filter, context).iterator();
return it.hasNext() ? it.next() : null;
}
// meh, Stream!
Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context);
Iterable<Document> bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context);
// Reference query
interface ReferenceFilter {
interface DocumentReferenceQuery {
Bson getFilter();
@ -49,21 +49,21 @@ public interface ReferenceLoader { @@ -49,21 +49,21 @@ public interface ReferenceLoader {
// TODO: Move apply method into something else that holds the collection and knows about single item/multi-item
// processing
default Stream<Document> apply(MongoCollection<Document> collection) {
return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false));
default Iterable<Document> apply(MongoCollection<Document> collection) {
return restoreOrder(collection.find(getFilter()).sort(getSort()));
}
default Stream<Document> restoreOrder(Stream<Document> stream) {
return stream;
default Iterable<Document> restoreOrder(Iterable<Document> documents) {
return documents;
}
static ReferenceFilter referenceFilter(Bson bson) {
static DocumentReferenceQuery referenceFilter(Bson bson) {
return () -> bson;
}
static ReferenceFilter singleReferenceFilter(Bson bson) {
static DocumentReferenceQuery singleReferenceFilter(Bson bson) {
return new ReferenceFilter() {
return new DocumentReferenceQuery() {
@Override
public Bson getFilter() {
@ -71,10 +71,10 @@ public interface ReferenceLoader { @@ -71,10 +71,10 @@ public interface ReferenceLoader {
}
@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();
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; @@ -17,24 +17,24 @@ package org.springframework.data.mongodb.core.convert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@ -56,61 +56,47 @@ import com.mongodb.client.MongoCollection; @@ -56,61 +56,47 @@ import com.mongodb.client.MongoCollection;
*/
public class ReferenceReader {
private final ParameterBindingDocumentCodec codec;
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
private final BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction;
private final Supplier<SpELContext> spelContextSupplier;
private final ParameterBindingDocumentCodec codec;
public ReferenceReader(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
Supplier<SpELContext> spelContextSupplier) {
this(() -> mappingContext, documentConversionFunction, spelContextSupplier);
this(() -> mappingContext, spelContextSupplier);
}
public ReferenceReader(
Supplier<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContextSupplier,
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
Supplier<SpELContext> spelContextSupplier) {
this.mappingContext = Lazy.of(mappingContextSupplier);
this.documentConversionFunction = documentConversionFunction;
this.spelContextSupplier = spelContextSupplier;
this.codec = new ParameterBindingDocumentCodec();
}
// TODO: Move documentConversionFunction to here. Having a contextual read allows projections in references
Object readReference(MongoPersistentProperty property, Object value,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction,
ResultConversionFunction resultConversionFunction) {
SpELContext spELContext = spelContextSupplier.get();
ReferenceFilter filter = computeFilter(property, value, spELContext);
ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext);
DocumentReferenceQuery filter = computeFilter(property, value, spELContext);
ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext);
Stream<Document> result = lookupFunction.apply(referenceContext, filter);
Iterable<Document> result = lookupFunction.apply(filter, referenceCollection);
if (property.isCollectionLike()) {
return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList());
if (!result.iterator().hasNext()) {
return null;
}
// TODO: retain target type and extract types here so the conversion function doesn't require type fiddling
// BiFunction<TypeInformation, Document, Object> instead of MongoPersistentProperty
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);
if (property.isCollectionLike()) {
return resultConversionFunction.apply(result, property.getTypeInformation());
}
return result.map(it -> documentConversionFunction.apply(property, it)).findFirst().orElse(null);
return resultConversionFunction.apply(result.iterator().next(), property.getTypeInformation());
}
private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value,
private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value,
SpELContext spELContext) {
if (value instanceof Iterable) {
@ -118,44 +104,43 @@ public class ReferenceReader { @@ -118,44 +104,43 @@ public class ReferenceReader {
}
if (value instanceof DBRef) {
return ReferenceContext.fromDBRef((DBRef) value);
return ReferenceCollection.fromDBRef((DBRef) value);
}
if (value instanceof Document) {
Document ref = (Document) value;
if (property.isAnnotationPresent(DocumentReference.class)) {
if (property.isDocumentReference()) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
DocumentReference documentReference = property.getDocumentReference();
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext,
() -> ref.get("db", String.class));
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
() -> ref.get("collection",
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
return new ReferenceContext(targetDatabase, targetCollection);
return new ReferenceCollection(targetDatabase, targetCollection);
}
return new ReferenceContext(ref.getString("db"), ref.get("collection",
return new ReferenceCollection(ref.getString("db"), ref.get("collection",
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
}
if (property.isAnnotationPresent(DocumentReference.class)) {
if (property.isDocumentReference()) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
DocumentReference documentReference = property.getDocumentReference();
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null);
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
() -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
Document sort = parseValueOrGet(documentReference.sort(), bindingContext, () -> null);
return new ReferenceContext(targetDatabase, targetCollection);
return new ReferenceCollection(targetDatabase, targetCollection);
}
return new ReferenceContext(null,
return new ReferenceCollection(null,
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
}
@ -201,9 +186,9 @@ public class ReferenceReader { @@ -201,9 +186,9 @@ public class ReferenceReader {
return ctx;
}
ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
DocumentReference documentReference = property.getDocumentReference();
String lookup = documentReference.lookup();
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null);
@ -217,7 +202,7 @@ public class ReferenceReader { @@ -217,7 +202,7 @@ public class ReferenceReader {
ors.add(decoded);
}
return new ListReferenceFilter(new Document("$or", ors), sort);
return new ListDocumentReferenceQuery(new Document("$or", ors), sort);
}
if (property.isMap() && value instanceof Map) {
@ -230,18 +215,18 @@ public class ReferenceReader { @@ -230,18 +215,18 @@ public class ReferenceReader {
filterMap.put(entry.getKey(), decoded);
}
return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap);
return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap);
}
return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
}
static class SingleReferenceFilter implements ReferenceFilter {
static class SingleDocumentReferenceQuery implements DocumentReferenceQuery {
Document filter;
Document sort;
public SingleReferenceFilter(Document filter, Document sort) {
public SingleDocumentReferenceQuery(Document filter, Document sort) {
this.filter = filter;
this.sort = sort;
}
@ -252,24 +237,24 @@ public class ReferenceReader { @@ -252,24 +237,24 @@ public class ReferenceReader {
}
@Override
public Stream<Document> apply(MongoCollection<Document> collection) {
public Iterable<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getFilter()).limit(1).first();
return result != null ? Stream.of(result) : Stream.empty();
return result != null ? Collections.singleton(result) : Collections.emptyList();
}
}
static class MapReferenceFilter implements ReferenceFilter {
static class MapDocumentReferenceQuery implements DocumentReferenceQuery {
Document filter;
Document sort;
Map<Object, Document> filterOrderMap;
private final Document filter;
private final Document sort;
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.filterOrderMap = filterOrderMap;
this.sort = sort;
this.filterOrderMap = filterOrderMap;
}
@Override
@ -283,45 +268,46 @@ public class ReferenceReader { @@ -283,45 +268,46 @@ public class ReferenceReader {
}
@Override
public Stream<Document> restoreOrder(Stream<Document> stream) {
public Iterable<Document> restoreOrder(Iterable<Document> documents) {
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()) {
String key = filterMapping.getKey().toString();
Optional<Document> first = collected.stream().filter(it -> {
Optional<Document> first = collected.stream()
.filter(it -> it.entrySet().containsAll(filterMapping.getValue().entrySet())).findFirst();
boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet());
return found;
}).findFirst();
targetMap.put(key, first.orElse(null));
targetMap.put(filterMapping.getKey().toString(), first.orElse(null));
}
return Stream.of(new Document(targetMap));
return Collections.singleton(new Document(targetMap));
}
}
static class ListReferenceFilter implements ReferenceFilter {
static class ListDocumentReferenceQuery implements DocumentReferenceQuery {
Document filter;
Document sort;
private final Document filter;
private final Document sort;
public ListDocumentReferenceQuery(Document filter, Document sort) {
public ListReferenceFilter(Document filter, Document sort) {
this.filter = filter;
this.sort = sort;
}
@Override
public Stream<Document> restoreOrder(Stream<Document> stream) {
public Iterable<Document> restoreOrder(Iterable<Document> documents) {
if (filter.containsKey("$or")) {
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() {
@ -347,7 +333,5 @@ public class ReferenceReader { @@ -347,7 +333,5 @@ public class ReferenceReader {
}
return referenceList.size();
}
}
}

45
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java

@ -15,13 +15,12 @@ @@ -15,13 +15,12 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import java.util.Collections;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import com.mongodb.DBRef;
@ -33,34 +32,38 @@ public interface ReferenceResolver { @@ -33,34 +32,38 @@ public interface ReferenceResolver {
@Nullable
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) {
return resolveReference(property, source, referenceReader, (ctx, filter) -> {
default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
ResultConversionFunction resultConversionFunction) {
return resolveReference(property, source, referenceReader, (filter, ctx) -> {
if (property.isCollectionLike() || property.isMap()) {
return getReferenceLoader().bulkFetch(filter, ctx);
}
Object target = getReferenceLoader().fetch(filter, ctx);
return target == null ? Stream.empty() : Stream.of(getReferenceLoader().fetch(filter, ctx));
});
return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetch(filter, ctx));
}, resultConversionFunction);
}
ReferenceLoader getReferenceLoader();
// TODO: ReferenceCollection
class ReferenceContext {
class ReferenceCollection {
@Nullable final String database;
final String collection;
@Nullable
private final String database;
private final String collection;
public ReferenceContext(@Nullable String database, String collection) {
public ReferenceCollection(@Nullable String database, String collection) {
this.database = database;
this.collection = collection;
}
static ReferenceContext fromDBRef(DBRef dbRef) {
return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName());
static ReferenceCollection fromDBRef(DBRef dbRef) {
return new ReferenceCollection(dbRef.getDatabaseName(), dbRef.getCollectionName());
}
public String getCollection() {
@ -72,4 +75,14 @@ public interface ReferenceResolver { @@ -72,4 +75,14 @@ public interface ReferenceResolver {
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 @@ -231,6 +231,15 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
return isAnnotationPresent(DBRef.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isDocumentReference()
*/
@Override
public boolean isDocumentReference() {
return isAnnotationPresent(DocumentReference.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef()
@ -240,6 +249,16 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope @@ -240,6 +249,16 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
return findAnnotation(DBRef.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDocumentReference()
*/
@Nullable
@Override
public DocumentReference getDocumentReference() {
return findAnnotation(DocumentReference.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty()

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 @@ @@ -16,10 +16,19 @@
package org.springframework.data.mongodb.core.mapping;
/**
* A custom pointer to a linked document to be used along with {@link DocumentReference} for storing the linkage value.
*
* @author Christoph Strobl
*/
@FunctionalInterface
// TODO: ObjectPointer or DocumentPointer
public interface ObjectReference<T> {
public interface DocumentPointer<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();
}

86
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java

@ -24,8 +24,69 @@ import java.lang.annotation.Target; @@ -24,8 +24,69 @@ import java.lang.annotation.Target;
import org.springframework.data.annotation.Reference;
/**
* A {@link DocumentReference} offers an alternative way of linking entities in MongoDB. While the goal is the same as
* when using {@link DBRef}, the store representation is different and can be literally anything, a single value, an
* entire {@link org.bson.Document}, basically everything that can be stored in MongoDB. By default, the mapping layer
* will use the referenced entities {@literal id} value for storage and retrieval.
*
* <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
* @since 3.3
* @see <a href="https://docs.mongodb.com/manual/reference/database-references/#std-label-document-references">MongoDB Reference Documentation</a>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@ -34,17 +95,38 @@ import org.springframework.data.annotation.Reference; @@ -34,17 +95,38 @@ import org.springframework.data.annotation.Reference;
public @interface DocumentReference {
/**
* The database the referred entity resides in.
* The database the linked entity resides in.
*
* @return empty String by default.
* @return empty String by default. Uses the default database provided buy the {@link org.springframework.data.mongodb.MongoDatabaseFactory}.
*/
String db() default "";
/**
* The database the linked entity resides in.
*
* @return empty String by default. Uses the property type for collection resolution.
*/
String collection() default "";
/**
* The single document lookup query. In case of an {@link java.util.Collection} or {@link java.util.Map} property
* the individual lookups are combined via an `$or` operator.
*
* @return an {@literal _id} based lookup.
*/
String lookup() default "{ '_id' : ?#{#target} }";
/**
* A specific sort.
*
* @return empty String by default.
*/
String sort() default "";
/**
* Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}.
*
* @return {@literal false} by default.
*/
boolean lazy() default false;
}

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 @@ -62,6 +62,15 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
*/
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}.
* 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 @@ -105,6 +114,16 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
@Nullable
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
* 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 { @@ -68,6 +68,11 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return delegate.isDbReference();
}
@Override
public boolean isDocumentReference() {
return delegate.isDocumentReference();
}
@Override
public boolean isExplicitIdProperty() {
return delegate.isExplicitIdProperty();
@ -94,6 +99,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty { @@ -94,6 +99,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return delegate.getDBRef();
}
@Override
@Nullable
public DocumentReference getDocumentReference() {
return delegate.getDocumentReference();
}
@Override
public boolean 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; @@ -23,6 +23,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Arrays;
import java.util.Collections;
@ -32,16 +33,17 @@ import java.util.Map; @@ -32,16 +33,17 @@ import java.util.Map;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.WritingConverter;
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.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.MongoClientExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
@ -51,14 +53,14 @@ import com.mongodb.client.MongoClient; @@ -51,14 +53,14 @@ import com.mongodb.client.MongoClient;
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
*/
@ExtendWith(MongoClientExtension.class)
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;
@ -71,7 +73,8 @@ public class MongoTemplateDocumentReferenceTests { @@ -71,7 +73,8 @@ public class MongoTemplateDocumentReferenceTests {
});
cfg.configureConversion(it -> {
it.customConverters(new ReferencableConverter());
it.customConverters(new ReferencableConverter(), new SimpleObjectRefWithReadingConverterToDocumentConverter(),
new DocumentToSimpleObjectRefWithReadingConverter());
});
cfg.configureMappingContext(it -> {
@ -84,7 +87,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -84,7 +87,7 @@ public class MongoTemplateDocumentReferenceTests {
template.flushDatabase();
}
@Test
@Test // GH-3602
void writeSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -102,12 +105,11 @@ public class MongoTemplateDocumentReferenceTests { @@ -102,12 +105,11 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
}
@Test
@Test // GH-3602
void writeMapTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
CollectionRefRoot source = new CollectionRefRoot();
source.id = "root-1";
source.mapValueRef = new LinkedHashMap<>();
@ -120,11 +122,10 @@ public class MongoTemplateDocumentReferenceTests { @@ -120,11 +122,10 @@ public class MongoTemplateDocumentReferenceTests {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
});
System.out.println("target: " + target.toJson());
assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2");
}
@Test
@Test // GH-3602
void writeCollectionOfSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -143,7 +144,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -143,7 +144,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
}
@Test
@Test // GH-3602
void writeObjectTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -161,7 +162,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -161,7 +162,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
}
@Test
@Test // GH-3602
void writeCollectionOfObjectTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -181,7 +182,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -181,7 +182,7 @@ public class MongoTemplateDocumentReferenceTests {
source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
}
@Test
@Test // GH-3602
void readSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -200,7 +201,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -200,7 +201,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readCollectionOfSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -220,7 +221,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -220,7 +221,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readLazySimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -245,7 +246,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -245,7 +246,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -266,7 +267,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -266,7 +267,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -287,7 +288,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -287,7 +288,7 @@ public class MongoTemplateDocumentReferenceTests {
.containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -307,7 +308,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -307,7 +308,7 @@ public class MongoTemplateDocumentReferenceTests {
assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readCollectionObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -328,7 +329,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -328,7 +329,7 @@ public class MongoTemplateDocumentReferenceTests {
.containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -351,7 +352,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -351,7 +352,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
}
@Test
@Test // GH-3602
void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -379,7 +380,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -379,7 +380,7 @@ public class MongoTemplateDocumentReferenceTests {
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
}
@Test
@Test // GH-3602
void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -401,7 +402,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -401,7 +402,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
@Test // GH-3602
void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
@ -429,7 +430,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -429,7 +430,7 @@ public class MongoTemplateDocumentReferenceTests {
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
@Test // GH-3602
void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -452,7 +453,7 @@ public class MongoTemplateDocumentReferenceTests { @@ -452,7 +453,7 @@ public class MongoTemplateDocumentReferenceTests {
.containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
@Test // GH-3602
void readMapOfReferences() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
@ -479,12 +480,414 @@ public class MongoTemplateDocumentReferenceTests { @@ -479,12 +480,414 @@ public class MongoTemplateDocumentReferenceTests {
});
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
System.out.println("result: " + result);
assertThat(result.getMapValueRef()).containsEntry("frodo",
new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
.containsEntry("bilbo",
new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
assertThat(result.getMapValueRef())
.containsEntry("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
.containsEntry("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
}
@Test // GH-3602
void loadLazyCyclicReference() {
WithRefA a = new WithRefA();
a.id = "a";
WithRefB b = new WithRefB();
b.id = "b";
a.toB = b;
b.lazyToA = a;
template.save(a);
template.save(b);
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
assertThat(loadedA).isNotNull();
assertThat(loadedA.getToB()).isNotNull();
LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
assertThat(proxy.isResolved()).isFalse();
assertThat(proxy.currentValue()).isNull();
});
}
@Test // GH-3602
void loadEagerCyclicReference() {
WithRefA a = new WithRefA();
a.id = "a";
WithRefB b = new WithRefB();
b.id = "b";
a.toB = b;
b.eagerToA = a;
template.save(a);
template.save(b);
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
assertThat(loadedA).isNotNull();
assertThat(loadedA.getToB()).isNotNull();
assertThat(loadedA.getToB().eagerToA).isSameAs(loadedA);
}
@Test // GH-3602
void loadAndStoreUnresolvedLazyDoesNotResolveTheProxy() {
String collectionB = template.getCollectionName(WithRefB.class);
WithRefA a = new WithRefA();
a.id = "a";
WithRefB b = new WithRefB();
b.id = "b";
a.toB = b;
b.lazyToA = a;
template.save(a);
template.save(b);
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
template.save(loadedA.getToB());
LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
assertThat(proxy.isResolved()).isFalse();
assertThat(proxy.currentValue()).isNull();
});
Document target = template.execute(db -> {
return db.getCollection(collectionB).find(Filters.eq("_id", "b")).first();
});
assertThat(target.get("lazyToA", Object.class)).isEqualTo("a");
}
@Test // GH-3602
void loadCollectionReferenceWithMissingRefs() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
// ref-1 is missing.
Document refSource = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
Arrays.asList("ref-1", "ref-2"));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
}
@Test // GH-3602
void queryForReference() {
WithRefB b = new WithRefB();
b.id = "b";
template.save(b);
WithRefA a = new WithRefA();
a.id = "a";
a.toB = b;
template.save(a);
WithRefA a2 = new WithRefA();
a2.id = "a2";
template.save(a2);
WithRefA loadedA = template.query(WithRefA.class).matching(where("toB").is(b)).firstValue();
assertThat(loadedA.getId()).isEqualTo(a.getId());
}
@Test // GH-3602
void queryForReferenceInCollection() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
Document shouldBeFound = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
Arrays.asList("ref-1", "ref-2"));
Document shouldNotBeFound = new Document("_id", "id-2").append("value", "v2").append("simpleValueRef",
Arrays.asList("ref-1"));
template.execute(db -> {
db.getCollection(rootCollectionName).insertOne(shouldBeFound);
db.getCollection(rootCollectionName).insertOne(shouldNotBeFound);
return null;
});
SimpleObjectRef objectRef = new SimpleObjectRef("ref-2", "some irrelevant value");
List<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
@ -556,16 +959,16 @@ public class MongoTemplateDocumentReferenceTests { @@ -556,16 +959,16 @@ public class MongoTemplateDocumentReferenceTests {
@Id String id;
String value;
}
@Getter
@Setter
static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) {
public SimpleObjectRefWithReadingConverter(String id, String value) {
super(id, value);
}
}
@Data
@ -609,41 +1012,94 @@ public class MongoTemplateDocumentReferenceTests { @@ -609,41 +1012,94 @@ public class MongoTemplateDocumentReferenceTests {
}
}
static class ReferencableConverter implements Converter<ReferenceAble, ObjectReference> {
static class ReferencableConverter implements Converter<ReferenceAble, DocumentPointer> {
@Nullable
@Override
public ObjectReference convert(ReferenceAble source) {
public DocumentPointer convert(ReferenceAble source) {
return source::toReference;
}
}
@WritingConverter
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) {
this.template = template;
Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref")
.find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first();
return new SimpleObjectRefWithReadingConverter(document.getString("_id"), document.getString("value"));
}
}
@WritingConverter
class SimpleObjectRefWithReadingConverterToDocumentConverter
implements Converter<SimpleObjectRefWithReadingConverter, DocumentPointer<Document>> {
@Nullable
@Override
public SimpleObjectRefWithReadingConverter convert(ObjectReference<Document> source) {
return template.findOne(query(where("id").is(source.getPointer().get("the-ref-key-you-did-not-expect"))),
SimpleObjectRefWithReadingConverter.class);
public DocumentPointer<Document> convert(SimpleObjectRefWithReadingConverter source) {
return () -> new Document("ref-key-from-custom-write-converter", source.getId());
}
}
@WritingConverter
class SimpleObjectRefWithReadingConverterToDocumentConverter
implements Converter<SimpleObjectRefWithReadingConverter, ObjectReference<Document>> {
@Getter
@Setter
static class WithRefA/* to B */ implements ReferenceAble {
@Id String id;
@DocumentReference WithRefB toB;
@Override
public Object toReference() {
return id;
}
}
@Getter
@Setter
@ToString
static class WithRefB/* to A */ implements ReferenceAble {
@Id String id;
@DocumentReference(lazy = true) WithRefA lazyToA;
@DocumentReference WithRefA eagerToA;
@Override
public Object toReference() {
return id;
}
}
static class ReferencedObject {}
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
@Nullable
@Override
public ObjectReference<Document> convert(SimpleObjectRefWithReadingConverter source) {
return () -> new Document("the-ref-key-you-did-not-expect", source.getId());
public DocumentPointer<Document> convert(ReferencedObject source) {
return () -> new Document("", source);
}
}
@Data
static class Book {
String id;
@DocumentReference(lookup = "{ 'acronym' : ?#{acc}, 'name' : ?#{n} }") Publisher publisher;
}
static class Publisher {
String id;
String acronym;
String name;
}
}

7
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java

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

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; @@ -48,6 +48,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@ -1487,4 +1488,32 @@ public class QueryMapperUnitTests { @@ -1487,4 +1488,32 @@ public class QueryMapperUnitTests {
@Field("renamed")
String renamed_fieldname_with_underscores;
}
static class WithDocumentReferences {
@DocumentReference
Sample sample;
@DocumentReference
SimpeEntityWithoutId noId;
@DocumentReference(lookup = "{ 'stringProperty' : ?#{stringProperty} }")
SimpeEntityWithoutId noIdButLookupQuery;
}
@Test
void xxx() {
Sample sample = new Sample();
sample.foo = "sample-id";
Query query = query(where("sample").is(sample));
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
context.getPersistentEntity(WithDocumentReferences.class));
System.out.println("mappedObject.toJson(): " + mappedObject.toJson());
}
}

7
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java

@ -18,11 +18,10 @@ package org.springframework.data.mongodb.performance; @@ -18,11 +18,10 @@ package org.springframework.data.mongodb.performance;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceLoader;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
import org.springframework.data.mongodb.core.convert.ReferenceReader;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -107,7 +106,7 @@ public class ReactivePerformanceTests { @@ -107,7 +106,7 @@ public class ReactivePerformanceTests {
@Nullable
@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;
}

15
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

@ -1434,4 +1434,19 @@ public abstract class AbstractPersonRepositoryIntegrationTests { @@ -1434,4 +1434,19 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
Person target = repository.findWithAggregationInProjection(alicia.getId());
assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase());
}
@Test // GH-3602
void executesQueryWithDocumentReferenceCorrectly() {
Person josh = new Person("Josh", "Long");
User dave = new User();
dave.id = "dave";
josh.setSpiritAnimal(dave);
operations.save(josh);
List<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; @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Unwrapped;
@ -74,6 +75,9 @@ public class Person extends Contact { @@ -74,6 +75,9 @@ public class Person extends Contact {
@Unwrapped.Nullable(prefix = "u") //
User unwrappedUser;
@DocumentReference
User spiritAnimal;
public Person() {
this(null, null);
@ -308,6 +312,14 @@ public class Person extends Contact { @@ -308,6 +312,14 @@ public class Person extends Contact {
this.unwrappedUser = unwrappedUser;
}
public User getSpiritAnimal() {
return spiritAnimal;
}
public void setSpiritAnimal(User spiritAnimal) {
this.spiritAnimal = spiritAnimal;
}
/*
* (non-Javadoc)
*

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

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

@ -1,6 +1,11 @@ @@ -1,6 +1,11 @@
[[new-features]]
= 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]]
== 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 @@ -480,6 +480,7 @@ The MappingMongoConverter can use metadata to drive the mapping of objects to do
* `@MongoId`: Applied at the field level to mark the field used for identity purpose. Accepts an optional `FieldType` to customize id conversion.
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the collection where the data will be stored.
* `@DBRef`: Applied at the field to indicate it is to be stored using a com.mongodb.DBRef.
* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. This can be a single value (the _id_ by default), or a `Document` provided via a converter.
* `@Indexed`: Applied at the field level to describe how to index the field.
* `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes.
* `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field.
@ -826,6 +827,370 @@ Required properties that are also defined as lazy loading ``DBRef`` and used as @@ -826,6 +827,370 @@ Required properties that are also defined as lazy loading ``DBRef`` and used as
TIP: Lazily loaded ``DBRef``s can be hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()` or some inline debug rendering invoking property getters.
Please consider to enable _trace_ logging for `org.springframework.data.mongodb.core.convert.DefaultDbRefResolver` to gain insight on `DBRef` resolution.
[[mapping-usage.linking]]
=== Using Document References
Using `@DocumentReference` offers an alternative way of linking entities in MongoDB.
While the goal is the same as when using <<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 Framework Events

Loading…
Cancel
Save