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.
This commit is contained in:
committed by
Mark Paluch
parent
48ac7e75ba
commit
6ed274bd9b
@@ -46,7 +46,7 @@ import org.springframework.data.mongodb.ClientSessionException;
|
||||
import org.springframework.data.mongodb.LazyLoadingException;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseUtils;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.objenesis.ObjenesisStd;
|
||||
@@ -117,7 +117,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
|
||||
*/
|
||||
@Override
|
||||
public Document fetch(DBRef dbRef) {
|
||||
return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef));
|
||||
return getReferenceLoader().fetch(DocumentReferenceQuery.singleReferenceFilter(Filters.eq("_id", dbRef.getId())),
|
||||
ReferenceCollection.fromDBRef(dbRef));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -157,9 +158,9 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db
|
||||
databaseSource.getCollectionName());
|
||||
}
|
||||
|
||||
List<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
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,15 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseUtils;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
|
||||
/**
|
||||
@@ -49,7 +43,7 @@ public class DefaultReferenceLoader implements ReferenceLoader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -44,24 +38,26 @@ public class DefaultReferenceResolver implements ReferenceResolver {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,19 @@ import static org.springframework.util.ReflectionUtils.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.cglib.proxy.Callback;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.Factory;
|
||||
import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.objenesis.ObjenesisStd;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -54,11 +50,12 @@ class LazyLoadingProxyGenerator {
|
||||
this.objenesis = new ObjenesisStd(true);
|
||||
}
|
||||
|
||||
public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
|
||||
BiFunction<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 {
|
||||
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 {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GET_SOURCE_METHOD.equals(method)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
|
||||
|
||||
if (ReflectionUtils.isToStringMethod(method)) {
|
||||
@@ -234,7 +238,7 @@ class LazyLoadingProxyGenerator {
|
||||
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
|
||||
// }
|
||||
|
||||
return referenceReader.readReference(property, source, lookupFunction);
|
||||
return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction);
|
||||
|
||||
} catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
|
||||
@@ -63,10 +63,9 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator;
|
||||
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.core.mapping.ObjectReference;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty;
|
||||
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
|
||||
@@ -124,6 +123,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
private SpELContext spELContext;
|
||||
private @Nullable EntityCallbacks entityCallbacks;
|
||||
private DocumentPointerFactory documentPointerFactory;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}.
|
||||
@@ -154,8 +154,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator);
|
||||
});
|
||||
|
||||
this.referenceReader = new ReferenceReader(mappingContext,
|
||||
(prop, document) -> this.read(prop.getActualType(), document), () -> spELContext);
|
||||
this.referenceReader = new ReferenceReader(mappingContext, () -> spELContext);
|
||||
this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,6 +366,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
|
||||
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
|
||||
|
||||
if (bson.get("_id") != null) {
|
||||
|
||||
Object existing = context.getPath().getPathItem(bson.get("_id"), entity.getCollection(), entity.getType());
|
||||
if (existing != null) {
|
||||
return (S) existing;
|
||||
}
|
||||
}
|
||||
|
||||
PreferredConstructor<S, MongoPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
|
||||
|
||||
ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null
|
||||
@@ -376,6 +384,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
S instance = instantiator.createInstance(entity, provider);
|
||||
|
||||
if (entity.requiresPropertyPopulation()) {
|
||||
|
||||
return populateProperties(context, entity, documentAccessor, evaluator, instance);
|
||||
}
|
||||
|
||||
@@ -451,7 +460,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
|
||||
}
|
||||
|
||||
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
|
||||
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
|
||||
evaluator);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -478,7 +488,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
|
||||
}
|
||||
|
||||
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
|
||||
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
|
||||
evaluator);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -494,7 +505,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
}
|
||||
|
||||
private void readAssociation(Association<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
|
||||
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
|
||||
return createDBRef(object, referringProperty);
|
||||
}
|
||||
|
||||
public Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) {
|
||||
|
||||
if (source instanceof LazyLoadingProxy) {
|
||||
return ((LazyLoadingProxy) source).getSource();
|
||||
}
|
||||
|
||||
if (referringProperty != null) {
|
||||
|
||||
if (referringProperty.isDbReference()) {
|
||||
return toDBRef(source, referringProperty);
|
||||
}
|
||||
if (referringProperty.isDocumentReference()) {
|
||||
return createDocumentPointer(source, referringProperty);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("oops - what's that " + source);
|
||||
}
|
||||
|
||||
Object createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) {
|
||||
|
||||
if (referringProperty == null) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (ClassUtils.isAssignableValue(referringProperty.getType(), source)
|
||||
&& conversionService.canConvert(referringProperty.getType(), DocumentPointer.class)) {
|
||||
return conversionService.convert(source, DocumentPointer.class).getPointer();
|
||||
}
|
||||
|
||||
if (ClassUtils.isAssignableValue(referringProperty.getAssociationTargetType(), source)) {
|
||||
return documentPointerFactory.computePointer(referringProperty, source, referringProperty.getActualType())
|
||||
.getPointer();
|
||||
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root entry method into write conversion. Adds a type discriminator to the {@link Document}. Shouldn't be called for
|
||||
* nested conversions.
|
||||
@@ -749,13 +806,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (prop.isAssociation()) {
|
||||
|
||||
if (conversionService.canConvert(valueType.getType(), ObjectReference.class)) {
|
||||
accessor.put(prop, conversionService.convert(obj, ObjectReference.class).getPointer());
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
accessor.put(prop, mappingContext.getPersistentEntity(prop.getAssociationTargetType())
|
||||
.getIdentifierAccessor(obj).getIdentifier());
|
||||
}
|
||||
accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext)
|
||||
.computePointer(prop, obj, valueType.getType()).getPointer());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -799,14 +851,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (property.isAssociation()) {
|
||||
return writeCollectionInternal(collection.stream().map(it -> {
|
||||
if (conversionService.canConvert(it.getClass(), ObjectReference.class)) {
|
||||
return conversionService.convert(it, ObjectReference.class).getPointer();
|
||||
if (conversionService.canConvert(it.getClass(), DocumentPointer.class)) {
|
||||
return conversionService.convert(it, DocumentPointer.class).getPointer();
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it)
|
||||
.getIdentifier();
|
||||
}
|
||||
}).collect(Collectors.toList()), ClassTypeInformation.from(ObjectReference.class), new BasicDBList());
|
||||
}).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new BasicDBList());
|
||||
}
|
||||
|
||||
if (property.hasExplicitWriteTarget()) {
|
||||
@@ -855,15 +907,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
if (conversions.isSimpleType(key.getClass())) {
|
||||
|
||||
String simpleKey = prepareMapKey(key.toString());
|
||||
if(property.isDbReference()) {
|
||||
if (property.isDbReference()) {
|
||||
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
|
||||
} else {
|
||||
if (conversionService.canConvert(value.getClass(), ObjectReference.class)) {
|
||||
document.put(simpleKey, conversionService.convert(value, ObjectReference.class).getPointer());
|
||||
if (conversionService.canConvert(value.getClass(), DocumentPointer.class)) {
|
||||
document.put(simpleKey, conversionService.convert(value, DocumentPointer.class).getPointer());
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value)
|
||||
.getIdentifier());
|
||||
document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType())
|
||||
.getIdentifierAccessor(value).getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.DBRef;
|
||||
@@ -77,7 +77,8 @@ public enum NoOpDbRefResolver implements DbRefResolver {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
LookupFunction lookupFunction,
|
||||
ResultConversionFunction resultConversionFunction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ public class QueryMapper {
|
||||
if (source instanceof Iterable) {
|
||||
BasicDBList result = new BasicDBList();
|
||||
for (Object element : (Iterable<?>) source) {
|
||||
result.add(createDbRefFor(element, property));
|
||||
result.add(createReferenceFor(element, property));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -614,12 +614,12 @@ public class QueryMapper {
|
||||
Document result = new Document();
|
||||
Document dbObject = (Document) source;
|
||||
for (String key : dbObject.keySet()) {
|
||||
result.put(key, createDbRefFor(dbObject.get(key), property));
|
||||
result.put(key, createReferenceFor(dbObject.get(key), property));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return createDbRefFor(source, property);
|
||||
return createReferenceFor(source, property);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -666,12 +666,16 @@ public class QueryMapper {
|
||||
return Collections.singletonMap(key, value).entrySet().iterator().next();
|
||||
}
|
||||
|
||||
private DBRef createDbRefFor(Object source, MongoPersistentProperty property) {
|
||||
private Object createReferenceFor(Object source, MongoPersistentProperty property) {
|
||||
|
||||
if (source instanceof DBRef) {
|
||||
return (DBRef) source;
|
||||
}
|
||||
|
||||
if(property != null && property.isDocumentReference()) {
|
||||
return converter.toDocumentReference(source, property);
|
||||
}
|
||||
|
||||
return converter.toDBRef(source, property);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
@@ -31,15 +31,15 @@ import com.mongodb.client.MongoCollection;
|
||||
public interface ReferenceLoader {
|
||||
|
||||
@Nullable
|
||||
default Document fetch(ReferenceFilter filter, ReferenceContext context) {
|
||||
return bulkFetch(filter, context).findFirst().orElse(null);
|
||||
default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) {
|
||||
|
||||
Iterator<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 {
|
||||
|
||||
// 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 Iterable<Document> restoreOrder(Iterable<Document> documents) {
|
||||
return documents;
|
||||
}
|
||||
|
||||
default Stream<Document> restoreOrder(Stream<Document> stream) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
static ReferenceFilter referenceFilter(Bson bson) {
|
||||
static DocumentReferenceQuery referenceFilter(Bson bson) {
|
||||
return () -> bson;
|
||||
}
|
||||
|
||||
static ReferenceFilter singleReferenceFilter(Bson bson) {
|
||||
static DocumentReferenceQuery singleReferenceFilter(Bson bson) {
|
||||
|
||||
return new ReferenceFilter() {
|
||||
return new DocumentReferenceQuery() {
|
||||
|
||||
@Override
|
||||
public Bson getFilter() {
|
||||
@@ -71,10 +71,10 @@ public interface ReferenceLoader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,24 +17,24 @@ package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.SpELContext;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
@@ -56,61 +56,47 @@ import com.mongodb.client.MongoCollection;
|
||||
*/
|
||||
public class ReferenceReader {
|
||||
|
||||
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
|
||||
private final Supplier<SpELContext> spelContextSupplier;
|
||||
private final ParameterBindingDocumentCodec codec;
|
||||
|
||||
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
|
||||
private final BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction;
|
||||
private final Supplier<SpELContext> spelContextSupplier;
|
||||
|
||||
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 (!result.iterator().hasNext()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (property.isCollectionLike()) {
|
||||
return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList());
|
||||
return resultConversionFunction.apply(result, property.getTypeInformation());
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return result.map(it -> documentConversionFunction.apply(property, it)).findFirst().orElse(null);
|
||||
return resultConversionFunction.apply(result.iterator().next(), property.getTypeInformation());
|
||||
}
|
||||
|
||||
private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value,
|
||||
private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value,
|
||||
SpELContext spELContext) {
|
||||
|
||||
if (value instanceof Iterable) {
|
||||
@@ -118,44 +104,43 @@ public class ReferenceReader {
|
||||
}
|
||||
|
||||
if (value instanceof DBRef) {
|
||||
return ReferenceContext.fromDBRef((DBRef) value);
|
||||
return ReferenceCollection.fromDBRef((DBRef) value);
|
||||
}
|
||||
|
||||
if (value instanceof Document) {
|
||||
|
||||
Document ref = (Document) value;
|
||||
|
||||
if (property.isAnnotationPresent(DocumentReference.class)) {
|
||||
if (property.isDocumentReference()) {
|
||||
|
||||
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
DocumentReference documentReference = property.getDocumentReference();
|
||||
|
||||
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext,
|
||||
() -> ref.get("db", String.class));
|
||||
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
|
||||
() -> ref.get("collection",
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
|
||||
return new ReferenceContext(targetDatabase, targetCollection);
|
||||
return new ReferenceCollection(targetDatabase, targetCollection);
|
||||
}
|
||||
|
||||
return new ReferenceContext(ref.getString("db"), ref.get("collection",
|
||||
return new ReferenceCollection(ref.getString("db"), ref.get("collection",
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
|
||||
}
|
||||
|
||||
if (property.isAnnotationPresent(DocumentReference.class)) {
|
||||
if (property.isDocumentReference()) {
|
||||
|
||||
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
DocumentReference documentReference = property.getDocumentReference();
|
||||
|
||||
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null);
|
||||
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
|
||||
() -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
|
||||
Document sort = parseValueOrGet(documentReference.sort(), bindingContext, () -> null);
|
||||
|
||||
return new ReferenceContext(targetDatabase, targetCollection);
|
||||
return new ReferenceCollection(targetDatabase, targetCollection);
|
||||
}
|
||||
|
||||
return new ReferenceContext(null,
|
||||
return new ReferenceCollection(null,
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
|
||||
}
|
||||
|
||||
@@ -201,9 +186,9 @@ public class ReferenceReader {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
|
||||
DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
|
||||
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
DocumentReference documentReference = property.getDocumentReference();
|
||||
String lookup = documentReference.lookup();
|
||||
|
||||
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null);
|
||||
@@ -217,7 +202,7 @@ public class ReferenceReader {
|
||||
ors.add(decoded);
|
||||
}
|
||||
|
||||
return new ListReferenceFilter(new Document("$or", ors), sort);
|
||||
return new ListDocumentReferenceQuery(new Document("$or", ors), sort);
|
||||
}
|
||||
|
||||
if (property.isMap() && value instanceof Map) {
|
||||
@@ -230,18 +215,18 @@ public class ReferenceReader {
|
||||
filterMap.put(entry.getKey(), decoded);
|
||||
}
|
||||
|
||||
return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap);
|
||||
return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap);
|
||||
}
|
||||
|
||||
return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
|
||||
return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
|
||||
}
|
||||
|
||||
static class SingleReferenceFilter implements ReferenceFilter {
|
||||
static class SingleDocumentReferenceQuery implements DocumentReferenceQuery {
|
||||
|
||||
Document filter;
|
||||
Document sort;
|
||||
|
||||
public SingleReferenceFilter(Document filter, Document sort) {
|
||||
public SingleDocumentReferenceQuery(Document filter, Document sort) {
|
||||
this.filter = filter;
|
||||
this.sort = sort;
|
||||
}
|
||||
@@ -252,24 +237,24 @@ public class ReferenceReader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<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 {
|
||||
}
|
||||
|
||||
@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 {
|
||||
}
|
||||
return referenceList.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.DBRef;
|
||||
@@ -33,34 +32,38 @@ public interface ReferenceResolver {
|
||||
|
||||
@Nullable
|
||||
Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<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 {
|
||||
return database;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface LookupFunction {
|
||||
Iterable<Document> apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ResultConversionFunction {
|
||||
Object apply(Object source, TypeInformation property);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +231,15 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
|
||||
return isAnnotationPresent(DBRef.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isDocumentReference()
|
||||
*/
|
||||
@Override
|
||||
public boolean isDocumentReference() {
|
||||
return isAnnotationPresent(DocumentReference.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef()
|
||||
@@ -240,6 +249,16 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
|
||||
return findAnnotation(DBRef.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDocumentReference()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public DocumentReference getDocumentReference() {
|
||||
return findAnnotation(DocumentReference.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
* @DocumentReference
|
||||
* private List<Account> accounts;
|
||||
* }
|
||||
*
|
||||
* Account account = ...
|
||||
*
|
||||
* mongoTemplate.insert(account);
|
||||
*
|
||||
* template.update(Person.class)
|
||||
* .matching(where("id").is(...))
|
||||
* .apply(new Update().push("accounts").value(account))
|
||||
* .first();
|
||||
* </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;
|
||||
*
|
||||
* @Field("publisher_ac")
|
||||
* @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }")
|
||||
* private Publisher publisher;
|
||||
* }
|
||||
*
|
||||
* public class Publisher {
|
||||
*
|
||||
* private ObjectId id;
|
||||
* private String acronym;
|
||||
* private String name;
|
||||
*
|
||||
* @DocumentReference(lazy = true)
|
||||
* private List<Book> books;
|
||||
* }
|
||||
*
|
||||
* @WritingConverter
|
||||
* public class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {
|
||||
*
|
||||
* public DocumentPointer<String> convert(Publisher source) {
|
||||
* return () -> source.getAcronym();
|
||||
* }
|
||||
* }
|
||||
* </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;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@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.
|
||||
|
||||
@@ -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 {
|
||||
return delegate.getDBRef();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DocumentReference getDocumentReference() {
|
||||
return delegate.getDocumentReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usePropertyAccess() {
|
||||
return delegate.usePropertyAccess();
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
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 {
|
||||
});
|
||||
|
||||
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 {
|
||||
template.flushDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void writeSimpleTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -102,12 +105,11 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void writeMapTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
|
||||
|
||||
CollectionRefRoot source = new CollectionRefRoot();
|
||||
source.id = "root-1";
|
||||
source.mapValueRef = new LinkedHashMap<>();
|
||||
@@ -120,11 +122,10 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
System.out.println("target: " + target.toJson());
|
||||
assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void writeCollectionOfSimpleTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -143,7 +144,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void writeObjectTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -161,7 +162,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void writeCollectionOfObjectTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -181,7 +182,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readSimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -200,7 +201,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readCollectionOfSimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -220,7 +221,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readLazySimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -245,7 +246,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -266,7 +267,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -287,7 +288,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readObjectReferenceFromDocumentType() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -307,7 +308,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readCollectionObjectReferenceFromDocumentType() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -328,7 +329,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readObjectReferenceFromDocumentDeclaringCollectionName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -351,7 +352,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -379,7 +380,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -401,7 +402,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
@@ -429,7 +430,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -452,7 +453,7 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
.containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test // GH-3602
|
||||
void readMapOfReferences() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
@@ -479,12 +480,414 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
System.out.println("result: " + result);
|
||||
|
||||
assertThat(result.getMapValueRef()).containsEntry("frodo",
|
||||
new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
|
||||
.containsEntry("bilbo",
|
||||
new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
assertThat(result.getMapValueRef())
|
||||
.containsEntry("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
|
||||
.containsEntry("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void loadLazyCyclicReference() {
|
||||
|
||||
WithRefA a = new WithRefA();
|
||||
a.id = "a";
|
||||
|
||||
WithRefB b = new WithRefB();
|
||||
b.id = "b";
|
||||
|
||||
a.toB = b;
|
||||
b.lazyToA = a;
|
||||
|
||||
template.save(a);
|
||||
template.save(b);
|
||||
|
||||
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
|
||||
assertThat(loadedA).isNotNull();
|
||||
assertThat(loadedA.getToB()).isNotNull();
|
||||
LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
|
||||
|
||||
assertThat(proxy.isResolved()).isFalse();
|
||||
assertThat(proxy.currentValue()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void loadEagerCyclicReference() {
|
||||
|
||||
WithRefA a = new WithRefA();
|
||||
a.id = "a";
|
||||
|
||||
WithRefB b = new WithRefB();
|
||||
b.id = "b";
|
||||
|
||||
a.toB = b;
|
||||
b.eagerToA = a;
|
||||
|
||||
template.save(a);
|
||||
template.save(b);
|
||||
|
||||
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
|
||||
|
||||
assertThat(loadedA).isNotNull();
|
||||
assertThat(loadedA.getToB()).isNotNull();
|
||||
assertThat(loadedA.getToB().eagerToA).isSameAs(loadedA);
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void loadAndStoreUnresolvedLazyDoesNotResolveTheProxy() {
|
||||
|
||||
String collectionB = template.getCollectionName(WithRefB.class);
|
||||
|
||||
WithRefA a = new WithRefA();
|
||||
a.id = "a";
|
||||
|
||||
WithRefB b = new WithRefB();
|
||||
b.id = "b";
|
||||
|
||||
a.toB = b;
|
||||
b.lazyToA = a;
|
||||
|
||||
template.save(a);
|
||||
template.save(b);
|
||||
|
||||
WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue();
|
||||
template.save(loadedA.getToB());
|
||||
|
||||
LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> {
|
||||
|
||||
assertThat(proxy.isResolved()).isFalse();
|
||||
assertThat(proxy.currentValue()).isNull();
|
||||
});
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(collectionB).find(Filters.eq("_id", "b")).first();
|
||||
});
|
||||
assertThat(target.get("lazyToA", Object.class)).isEqualTo("a");
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void loadCollectionReferenceWithMissingRefs() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
|
||||
// ref-1 is missing.
|
||||
Document refSource = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
|
||||
Arrays.asList("ref-1", "ref-2"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void queryForReference() {
|
||||
|
||||
WithRefB b = new WithRefB();
|
||||
b.id = "b";
|
||||
template.save(b);
|
||||
|
||||
WithRefA a = new WithRefA();
|
||||
a.id = "a";
|
||||
a.toB = b;
|
||||
template.save(a);
|
||||
|
||||
WithRefA a2 = new WithRefA();
|
||||
a2.id = "a2";
|
||||
template.save(a2);
|
||||
|
||||
WithRefA loadedA = template.query(WithRefA.class).matching(where("toB").is(b)).firstValue();
|
||||
assertThat(loadedA.getId()).isEqualTo(a.getId());
|
||||
}
|
||||
|
||||
@Test // GH-3602
|
||||
void queryForReferenceInCollection() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
|
||||
Document shouldBeFound = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
|
||||
Arrays.asList("ref-1", "ref-2"));
|
||||
Document shouldNotBeFound = new Document("_id", "id-2").append("value", "v2").append("simpleValueRef",
|
||||
Arrays.asList("ref-1"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(rootCollectionName).insertOne(shouldBeFound);
|
||||
db.getCollection(rootCollectionName).insertOne(shouldNotBeFound);
|
||||
return null;
|
||||
});
|
||||
|
||||
SimpleObjectRef objectRef = new SimpleObjectRef("ref-2", "some irrelevant value");
|
||||
|
||||
List<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 {
|
||||
|
||||
@Id String id;
|
||||
String value;
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
|
||||
|
||||
public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) {
|
||||
public SimpleObjectRefWithReadingConverter(String id, String value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -609,41 +1012,94 @@ public class MongoTemplateDocumentReferenceTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class ReferencableConverter implements Converter<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> {
|
||||
|
||||
private final MongoTemplate template;
|
||||
|
||||
public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
implements Converter<DocumentPointer<Document>, SimpleObjectRefWithReadingConverter> {
|
||||
|
||||
@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 SimpleObjectRefWithReadingConverter convert(DocumentPointer<Document> source) {
|
||||
|
||||
Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref")
|
||||
.find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first();
|
||||
return new SimpleObjectRefWithReadingConverter(document.getString("_id"), document.getString("value"));
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
class SimpleObjectRefWithReadingConverterToDocumentConverter
|
||||
implements Converter<SimpleObjectRefWithReadingConverter, ObjectReference<Document>> {
|
||||
implements Converter<SimpleObjectRefWithReadingConverter, 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(SimpleObjectRefWithReadingConverter source) {
|
||||
return () -> new Document("ref-key-from-custom-write-converter", source.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
static class WithRefA/* to B */ implements ReferenceAble {
|
||||
|
||||
@Id String id;
|
||||
@DocumentReference WithRefB toB;
|
||||
|
||||
@Override
|
||||
public Object toReference() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
static class WithRefB/* to A */ implements ReferenceAble {
|
||||
|
||||
@Id String id;
|
||||
@DocumentReference(lazy = true) WithRefA lazyToA;
|
||||
|
||||
@DocumentReference WithRefA eagerToA;
|
||||
|
||||
@Override
|
||||
public Object toReference() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
static class ReferencedObject {}
|
||||
|
||||
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.DocumentTestUtils;
|
||||
@@ -64,8 +65,6 @@ class DefaultDbRefResolverUnitTests {
|
||||
when(factoryMock.getMongoDatabase()).thenReturn(dbMock);
|
||||
when(dbMock.getCollection(anyString(), any(Class.class))).thenReturn(collectionMock);
|
||||
when(collectionMock.find(any(Document.class))).thenReturn(cursorMock);
|
||||
when(cursorMock.sort(any(Document.class))).thenReturn(cursorMock);
|
||||
when(cursorMock.spliterator()).thenReturn(Collections.<Document> emptyList().spliterator());
|
||||
|
||||
resolver = new DefaultDbRefResolver(factoryMock);
|
||||
}
|
||||
@@ -116,7 +115,7 @@ class DefaultDbRefResolverUnitTests {
|
||||
DBRef ref1 = new DBRef("collection-1", o1.get("_id"));
|
||||
DBRef ref2 = new DBRef("collection-1", o2.get("_id"));
|
||||
|
||||
when(cursorMock.spliterator()).thenReturn(Arrays.asList(o2, o1).spliterator());
|
||||
when(cursorMock.into(any())).then(invocation -> Arrays.asList(o2, o1));
|
||||
|
||||
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(o1, o2);
|
||||
}
|
||||
@@ -129,7 +128,7 @@ class DefaultDbRefResolverUnitTests {
|
||||
DBRef ref1 = new DBRef("collection-1", document.get("_id"));
|
||||
DBRef ref2 = new DBRef("collection-1", document.get("_id"));
|
||||
|
||||
when(cursorMock.spliterator()).thenReturn(Arrays.asList(document).spliterator());
|
||||
when(cursorMock.into(any())).then(invocation -> Arrays.asList(document));
|
||||
|
||||
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
|
||||
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.FieldType;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
@@ -1487,4 +1488,32 @@ public class QueryMapperUnitTests {
|
||||
@Field("renamed")
|
||||
String renamed_fieldname_with_underscores;
|
||||
}
|
||||
|
||||
static class WithDocumentReferences {
|
||||
|
||||
@DocumentReference
|
||||
Sample sample;
|
||||
|
||||
@DocumentReference
|
||||
SimpeEntityWithoutId noId;
|
||||
|
||||
@DocumentReference(lookup = "{ 'stringProperty' : ?#{stringProperty} }")
|
||||
SimpeEntityWithoutId noIdButLookupQuery;
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void xxx() {
|
||||
|
||||
Sample sample = new Sample();
|
||||
sample.foo = "sample-id";
|
||||
|
||||
Query query = query(where("sample").is(sample));
|
||||
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(WithDocumentReferences.class));
|
||||
|
||||
System.out.println("mappedObject.toJson(): " + mappedObject.toJson());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +18,10 @@ package org.springframework.data.mongodb.performance;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceReader;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -107,7 +106,7 @@ public class ReactivePerformanceTests {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped;
|
||||
|
||||
@@ -74,6 +75,9 @@ public class Person extends Contact {
|
||||
@Unwrapped.Nullable(prefix = "u") //
|
||||
User unwrappedUser;
|
||||
|
||||
@DocumentReference
|
||||
User spiritAnimal;
|
||||
|
||||
public Person() {
|
||||
|
||||
this(null, null);
|
||||
@@ -308,6 +312,14 @@ public class Person extends Contact {
|
||||
this.unwrappedUser = unwrappedUser;
|
||||
}
|
||||
|
||||
public User getSpiritAnimal() {
|
||||
return spiritAnimal;
|
||||
}
|
||||
|
||||
public void setSpiritAnimal(User spiritAnimal) {
|
||||
this.spiritAnimal = spiritAnimal;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -480,6 +480,7 @@ The MappingMongoConverter can use metadata to drive the mapping of objects to do
|
||||
* `@MongoId`: Applied at the field level to mark the field used for identity purpose. Accepts an optional `FieldType` to customize id conversion.
|
||||
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the collection where the data will be stored.
|
||||
* `@DBRef`: Applied at the field to indicate it is to be stored using a com.mongodb.DBRef.
|
||||
* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. This can be a single value (the _id_ by default), or a `Document` provided via a converter.
|
||||
* `@Indexed`: Applied at the field level to describe how to index the field.
|
||||
* `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes.
|
||||
* `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field.
|
||||
@@ -826,6 +827,370 @@ Required properties that are also defined as lazy loading ``DBRef`` and used as
|
||||
TIP: Lazily loaded ``DBRef``s can be hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()` or some inline debug rendering invoking property getters.
|
||||
Please consider to enable _trace_ logging for `org.springframework.data.mongodb.core.convert.DefaultDbRefResolver` to gain insight on `DBRef` resolution.
|
||||
|
||||
[[mapping-usage.linking]]
|
||||
=== Using Document References
|
||||
|
||||
Using `@DocumentReference` offers an alternative way of linking entities in MongoDB.
|
||||
While the goal is the same as when using <<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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user