Enhance support for linking entities.

Add initial support for an alternative to the existing DBRef scenario.
The enhancement allows to store and retrieve linked entites via their id or a customizable lookup query.

Original pull request: #3647.
Closes #3602.
This commit is contained in:
Christoph Strobl
2021-03-05 07:27:02 +01:00
committed by Mark Paluch
parent f1354c4508
commit a51c96298f
18 changed files with 1779 additions and 35 deletions

View File

@@ -35,7 +35,7 @@ import com.mongodb.DBRef;
* @author Mark Paluch
* @since 1.4
*/
public interface DbRefResolver {
public interface DbRefResolver extends ReferenceResolver {
/**
* Resolves the given {@link DBRef} into an object of the given {@link MongoPersistentProperty}'s type. The method

View File

@@ -46,6 +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.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
@@ -67,7 +68,7 @@ import com.mongodb.client.model.Filters;
* @author Mark Paluch
* @since 1.4
*/
public class DefaultDbRefResolver implements DbRefResolver {
public class DefaultDbRefResolver extends DefaultReferenceResolver implements DbRefResolver, ReferenceResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDbRefResolver.class);
@@ -82,6 +83,8 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) {
super(new DefaultReferenceLoader(mongoDbFactory));
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
this.mongoDbFactory = mongoDbFactory;
@@ -114,17 +117,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
*/
@Override
public Document fetch(DBRef dbRef) {
MongoCollection<Document> mongoCollection = getCollection(dbRef);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Fetching DBRef '{}' from {}.{}.", dbRef.getId(),
StringUtils.hasText(dbRef.getDatabaseName()) ? dbRef.getDatabaseName()
: mongoCollection.getNamespace().getDatabaseName(),
dbRef.getCollectionName());
}
return mongoCollection.find(Filters.eq("_id", dbRef.getId())).first();
return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef));
}
/*
@@ -164,9 +157,9 @@ public class DefaultDbRefResolver implements DbRefResolver {
databaseSource.getCollectionName());
}
List<Document> result = mongoCollection //
.find(new Document("_id", new Document("$in", ids))) //
.into(new ArrayList<>());
List<Document> result = getReferenceLoader()
.bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next()))
.collect(Collectors.toList());
return ids.stream() //
.flatMap(id -> documentWithId(id, result)) //
@@ -504,4 +497,10 @@ public class DefaultDbRefResolver implements DbRefResolver {
return MongoDatabaseUtils.getDatabase(dbref.getDatabaseName(), mongoDbFactory)
.getCollection(dbref.getCollectionName(), Document.class);
}
protected MongoCollection<Document> getCollection(ReferenceContext context) {
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
Document.class);
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.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.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
/**
* @author Christoph Strobl
*/
public class DefaultReferenceLoader implements ReferenceLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultReferenceLoader.class);
private final MongoDatabaseFactory mongoDbFactory;
public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) {
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
this.mongoDbFactory = mongoDbFactory;
}
@Override
public Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context) {
MongoCollection<Document> collection = getCollection(context);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Bulk fetching {} from {}.{}.", filter,
StringUtils.hasText(context.getDatabase()) ? context.getDatabase()
: collection.getNamespace().getDatabaseName(),
context.getCollection());
}
return filter.apply(collection);
}
protected MongoCollection<Document> getCollection(ReferenceContext context) {
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
Document.class);
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.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;
/**
* @author Christoph Strobl
*/
public class DefaultReferenceResolver implements ReferenceResolver {
private final ReferenceLoader referenceLoader;
public DefaultReferenceResolver(ReferenceLoader referenceLoader) {
this.referenceLoader = referenceLoader;
}
@Override
public ReferenceLoader getReferenceLoader() {
return referenceLoader;
}
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
if (isLazyReference(property)) {
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction);
}
return referenceReader.readReference(property, source, lookupFunction);
}
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction);
}
protected boolean isLazyReference(MongoPersistentProperty property) {
if (property.findAnnotation(DocumentReference.class) != null) {
return property.findAnnotation(DocumentReference.class).lazy();
}
return property.getDBRef() != null && property.getDBRef().lazy();
}
}

View File

@@ -0,0 +1,253 @@
/*
* 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 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.mapping.MongoPersistentProperty;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ReflectionUtils;
/**
* @author Christoph Strobl
*/
class LazyLoadingProxyGenerator {
private final ObjenesisStd objenesis;
private final ReferenceReader referenceReader;
public LazyLoadingProxyGenerator(ReferenceReader referenceReader) {
this.referenceReader = referenceReader;
this.objenesis = new ObjenesisStd(true);
}
public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction);
if (!propertyType.isInterface()) {
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
factory.setCallbacks(new Callback[] { interceptor });
return factory;
}
ProxyFactory proxyFactory = new ProxyFactory();
for (Class<?> type : propertyType.getInterfaces()) {
proxyFactory.addInterface(type);
}
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addInterface(propertyType);
proxyFactory.addAdvice(interceptor);
return proxyFactory.getProxy(LazyLoadingProxy.class.getClassLoader());
}
/**
* Returns the CGLib enhanced type for the given source type.
*
* @param type
* @return
*/
private Class<?> getEnhancedTypeFor(Class<?> type) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
return enhancer.createClass();
}
public static class LazyLoadingInterceptor
implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable {
private final ReferenceReader referenceReader;
MongoPersistentProperty property;
private volatile boolean resolved;
private @org.springframework.lang.Nullable Object result;
private Object source;
private BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction;
private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD;
{
try {
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
this.property = property;
this.source = source;
this.referenceReader = reader;
this.lookupFunction = lookupFunction;
}
@Nullable
@Override
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (INITIALIZE_METHOD.equals(method)) {
return ensureResolved();
}
if (TO_DBREF_METHOD.equals(method)) {
return null;
}
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
if (ReflectionUtils.isToStringMethod(method)) {
return proxyToString(proxy);
}
if (ReflectionUtils.isEqualsMethod(method)) {
return proxyEquals(proxy, args[0]);
}
if (ReflectionUtils.isHashCodeMethod(method)) {
return proxyHashCode(proxy);
}
// DATAMONGO-1076 - finalize methods should not trigger proxy initialization
if (FINALIZE_METHOD.equals(method)) {
return null;
}
}
Object target = ensureResolved();
if (target == null) {
return null;
}
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
private Object ensureResolved() {
if (!resolved) {
this.result = resolve();
this.resolved = true;
}
return this.result;
}
private String proxyToString(Object source) {
StringBuilder description = new StringBuilder();
if (source != null) {
description.append(source);
} else {
description.append(System.identityHashCode(source));
}
description.append("$").append(LazyLoadingProxy.class.getSimpleName());
return description.toString();
}
private boolean proxyEquals(@org.springframework.lang.Nullable Object proxy, Object that) {
if (!(that instanceof LazyLoadingProxy)) {
return false;
}
if (that == proxy) {
return true;
}
return proxyToString(proxy).equals(that.toString());
}
private int proxyHashCode(@org.springframework.lang.Nullable Object proxy) {
return proxyToString(proxy).hashCode();
}
@org.springframework.lang.Nullable
private synchronized Object resolve() {
if (resolved) {
// if (LOGGER.isTraceEnabled()) {
// LOGGER.trace("Accessing already resolved lazy loading property {}.{}",
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
// }
return result;
}
try {
// if (LOGGER.isTraceEnabled()) {
// LOGGER.trace("Resolving lazy loading property {}.{}",
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
// }
return referenceReader.readReference(property, source, lookupFunction);
} catch (RuntimeException ex) {
throw ex;
// DataAccessException translatedException = this.exceptionTranslator.translateExceptionIfPossible(ex);
//
// if (translatedException instanceof ClientSessionException) {
// throw new LazyLoadingException("Unable to lazily resolve DBRef! Invalid session state.", ex);
// }
// throw new LazyLoadingException("Unable to lazily resolve DBRef!",
// translatedException != null ? translatedException : ex);
}
}
}
}

View File

@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.bson.Document;
import org.bson.codecs.Codec;
@@ -62,8 +63,10 @@ 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.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;
@@ -112,6 +115,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
protected final QueryMapper idMapper;
protected final DbRefResolver dbRefResolver;
protected final DefaultDbRefProxyHandler dbRefProxyHandler;
protected final ReferenceReader referenceReader;
protected @Nullable ApplicationContext applicationContext;
protected MongoTypeMapper typeMapper;
@@ -136,12 +140,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.dbRefResolver = dbRefResolver;
this.mappingContext = mappingContext;
this.typeMapper = new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext,
this::getWriteTarget);
this.idMapper = new QueryMapper(this);
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
this.dbRefProxyHandler = new DefaultDbRefProxyHandler(spELContext, mappingContext,
(prop, bson, evaluator, path) -> {
@@ -149,6 +153,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
ConversionContext context = getConversionContext(path);
return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator);
});
this.referenceReader = new ReferenceReader(mappingContext,
(prop, document) -> this.read(prop.getActualType(), document), () -> spELContext);
}
/**
@@ -376,8 +383,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
private <S> S populateProperties(ConversionContext context, MongoPersistentEntity<S> entity,
DocumentAccessor documentAccessor,
SpELExpressionEvaluator evaluator, S instance) {
DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) {
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
conversionService);
@@ -423,8 +429,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
@Nullable
private Object readIdValue(ConversionContext context, SpELExpressionEvaluator evaluator,
MongoPersistentProperty idProperty,
Object rawId) {
MongoPersistentProperty idProperty, Object rawId) {
String expression = idProperty.getSpelExpression();
Object resolvedValue = expression != null ? evaluator.evaluate(expression) : rawId;
@@ -434,8 +439,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private void readProperties(ConversionContext context, MongoPersistentEntity<?> entity,
PersistentPropertyAccessor<?> accessor, DocumentAccessor documentAccessor,
MongoDbPropertyValueProvider valueProvider,
SpELExpressionEvaluator evaluator) {
MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) {
DbRefResolverCallback callback = null;
@@ -493,20 +497,38 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) {
MongoPersistentProperty property = association.getInverse();
Object value = documentAccessor.get(property);
final Object value = documentAccessor.get(property);
if (value == null) {
return;
}
if (property.isAnnotationPresent(DocumentReference.class)) {
// quite unusual but sounds like worth having?
if (conversionService.canConvert(ObjectReference.class, property.getActualType())) {
// collection like special treatment
accessor.setProperty(property, conversionService.convert(new ObjectReference() {
@Override
public Object getPointer() {
return value;
}
}, property.getActualType()));
} else {
accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader));
}
return;
}
DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
}
@Nullable
private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor,
MongoPersistentProperty prop,
MongoPersistentEntity<?> unwrappedEntity) {
MongoPersistentProperty prop, MongoPersistentEntity<?> unwrappedEntity) {
if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
return read(context, unwrappedEntity, (Document) documentAccessor.getDocument());
@@ -725,6 +747,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return;
}
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());
}
return;
}
/*
* If we have a LazyLoadingProxy we make sure it is initialized first.
*/
@@ -763,6 +797,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (!property.isDbReference()) {
if (property.isAssociation()) {
return writeCollectionInternal(collection.stream().map(it -> {
if (conversionService.canConvert(it.getClass(), ObjectReference.class)) {
return conversionService.convert(it, ObjectReference.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());
}
if (property.hasExplicitWriteTarget()) {
return writeCollectionInternal(collection, new FieldTypeInformation<>(property), new ArrayList<>());
}
@@ -795,7 +841,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(property, "PersistentProperty must not be null!");
if (!property.isDbReference()) {
if (!property.isAssociation()) {
return writeMapInternal(map, new Document(), property.getTypeInformation());
}
@@ -809,7 +855,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.isSimpleType(key.getClass())) {
String simpleKey = prepareMapKey(key.toString());
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
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());
} else {
// just take the id as a reference
document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value)
.getIdentifier());
}
}
} else {
throw new MappingException("Cannot use a complex object as a key value.");
@@ -1447,8 +1503,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
T target = null;
if (document != null) {
maybeEmitEvent(
new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
maybeEmitEvent(new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
target = (T) readDocument(context, document, type);
}
@@ -1541,9 +1596,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
@SuppressWarnings("ConstantConditions")
private <T extends Object> T doConvert(Object value, Class<? extends T> target, @Nullable Class<? extends T> fallback) {
private <T extends Object> T doConvert(Object value, Class<? extends T> target,
@Nullable Class<? extends T> fallback) {
if(conversionService.canConvert(value.getClass(), target) || fallback == null) {
if (conversionService.canConvert(value.getClass(), target) || fallback == null) {
return conversionService.convert(value, target);
}
return conversionService.convert(value, fallback);

View File

@@ -16,8 +16,12 @@
package org.springframework.data.mongodb.core.convert;
import java.util.List;
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.MongoPersistentProperty;
import org.springframework.lang.Nullable;
@@ -69,4 +73,16 @@ public enum NoOpDbRefResolver implements DbRefResolver {
private <T> T handle() throws UnsupportedOperationException {
throw new UnsupportedOperationException("DBRef resolution is not supported!");
}
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
return null;
}
@Override
public ReferenceLoader getReferenceLoader() {
return handle();
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.stream.Stream;
import java.util.stream.StreamSupport;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoCollection;
/**
* @author Christoph Strobl
*/
public interface ReferenceLoader {
@Nullable
default Document fetch(ReferenceFilter filter, ReferenceContext context) {
return bulkFetch(filter, context).findFirst().orElse(null);
}
Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context);
interface ReferenceFilter {
Bson getFilter();
default Bson getSort() {
return new Document();
}
default Stream<Document> apply(MongoCollection<Document> collection) {
return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false));
}
default Stream<Document> restoreOrder(Stream<Document> stream) {
return stream;
}
static ReferenceFilter referenceFilter(Bson bson) {
return () -> bson;
}
static ReferenceFilter singleReferenceFilter(Bson bson) {
return new ReferenceFilter() {
@Override
public Bson getFilter() {
return bson;
}
@Override
public Stream<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getFilter()).sort(getSort()).limit(1).first();
return result != null ? Stream.of(result) : Stream.empty();
}
};
}
}
}

View File

@@ -0,0 +1,350 @@
/*
* 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.ArrayList;
import java.util.Collection;
import java.util.Iterator;
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.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.mongodb.util.json.ValueProvider;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Streamable;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.mongodb.DBRef;
import com.mongodb.client.MongoCollection;
/**
* @author Christoph Strobl
*/
public class ReferenceReader {
private final ParameterBindingDocumentCodec codec;
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
private final BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction;
private final Supplier<SpELContext> spelContextSupplier;
public ReferenceReader(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
Supplier<SpELContext> spelContextSupplier) {
this(() -> mappingContext, documentConversionFunction, 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();
}
Object readReference(MongoPersistentProperty property, Object value,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
SpELContext spELContext = spelContextSupplier.get();
ReferenceFilter filter = computeFilter(property, value, spELContext);
ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext);
Stream<Document> result = lookupFunction.apply(referenceContext, filter);
if (property.isCollectionLike()) {
return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList());
}
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);
}
private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value,
SpELContext spELContext) {
if (value instanceof Iterable) {
value = ((Iterable<?>) value).iterator().next();
}
if (value instanceof DBRef) {
return ReferenceContext.fromDBRef((DBRef) value);
}
if (value instanceof Document) {
Document ref = (Document) value;
if (property.isAnnotationPresent(DocumentReference.class)) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
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 ReferenceContext(ref.getString("db"), ref.get("collection",
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
}
if (property.isAnnotationPresent(DocumentReference.class)) {
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
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 ReferenceContext(null,
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
}
@Nullable
private <T> T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<T> defaultValue) {
if (!StringUtils.hasText(value)) {
return defaultValue.get();
}
if (!BsonUtils.isJsonDocument(value) && value.contains("?#{")) {
String s = "{ 'target-value' : " + value + "}";
T evaluated = (T) new ParameterBindingDocumentCodec().decode(s, bindingContext).get("target-value ");
return evaluated != null ? evaluated : defaultValue.get();
}
T evaluated = (T) bindingContext.evaluateExpression(value);
return evaluated != null ? evaluated : defaultValue.get();
}
ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) {
return new ParameterBindingContext(valueProviderFor(source), spELContext.getParser(),
() -> evaluationContextFor(property, source, spELContext));
}
ValueProvider valueProviderFor(Object source) {
return (index) -> {
if (source instanceof Document) {
return Streamable.of(((Document) source).values()).toList().get(index);
}
return source;
};
}
EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
EvaluationContext ctx = spELContext.getEvaluationContext(source);
ctx.setVariable("target", source);
ctx.setVariable(property.getName(), source);
return ctx;
}
ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
String lookup = documentReference.lookup();
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null);
if (property.isCollectionLike() && value instanceof Collection) {
List<Document> ors = new ArrayList<>();
for (Object entry : (Collection) value) {
Document decoded = codec.decode(lookup, bindingContext(property, entry, spELContext));
ors.add(decoded);
}
return new ListReferenceFilter(new Document("$or", ors), sort);
}
if (property.isMap() && value instanceof Map) {
Map<Object, Document> filterMap = new LinkedHashMap<>();
for (Entry entry : ((Map<Object, Object>) value).entrySet()) {
Document decoded = codec.decode(lookup, bindingContext(property, entry.getValue(), spELContext));
filterMap.put(entry.getKey(), decoded);
}
return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap);
}
return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
}
static class SingleReferenceFilter implements ReferenceFilter {
Document filter;
Document sort;
public SingleReferenceFilter(Document filter, Document sort) {
this.filter = filter;
this.sort = sort;
}
@Override
public Bson getFilter() {
return filter;
}
@Override
public Stream<Document> apply(MongoCollection<Document> collection) {
Document result = collection.find(getFilter()).limit(1).first();
return result != null ? Stream.of(result) : Stream.empty();
}
}
static class MapReferenceFilter implements ReferenceFilter {
Document filter;
Document sort;
Map<Object, Document> filterOrderMap;
public MapReferenceFilter(Document filter, Document sort, Map<Object, Document> filterOrderMap) {
this.filter = filter;
this.filterOrderMap = filterOrderMap;
this.sort = sort;
}
@Override
public Bson getFilter() {
return filter;
}
@Override
public Bson getSort() {
return sort;
}
@Override
public Stream<Document> restoreOrder(Stream<Document> stream) {
Map<String, Object> targetMap = new LinkedHashMap<>();
List<Document> collected = stream.collect(Collectors.toList());
for (Entry<Object, Document> filterMapping : filterOrderMap.entrySet()) {
String key = filterMapping.getKey().toString();
Optional<Document> first = collected.stream().filter(it -> {
boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet());
return found;
}).findFirst();
targetMap.put(key, first.orElse(null));
}
return Stream.of(new Document(targetMap));
}
}
static class ListReferenceFilter implements ReferenceFilter {
Document filter;
Document sort;
public ListReferenceFilter(Document filter, Document sort) {
this.filter = filter;
this.sort = sort;
}
@Override
public Stream<Document> restoreOrder(Stream<Document> stream) {
if (filter.containsKey("$or")) {
List<Document> ors = filter.get("$or", List.class);
return stream.sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2));
}
return stream;
}
public Document getFilter() {
return filter;
}
@Override
public Document getSort() {
return sort;
}
int compareAgainstReferenceIndex(List<Document> referenceList, Document document1, Document document2) {
for (int i = 0; i < referenceList.size(); i++) {
Set<Entry<String, Object>> entries = referenceList.get(i).entrySet();
if (document1.entrySet().containsAll(entries)) {
return -1;
}
if (document2.entrySet().containsAll(entries)) {
return 1;
}
}
return referenceList.size();
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.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.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import com.mongodb.DBRef;
/**
* @author Christoph Strobl
*/
public interface ReferenceResolver {
@Nullable
Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction);
default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader) {
return resolveReference(property, source, referenceReader, (ctx, filter) -> {
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));
});
}
ReferenceLoader getReferenceLoader();
class ReferenceContext {
@Nullable final String database;
final String collection;
public ReferenceContext(@Nullable String database, String collection) {
this.database = database;
this.collection = collection;
}
static ReferenceContext fromDBRef(DBRef dbRef) {
return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName());
}
public String getCollection() {
return collection;
}
@Nullable
public String getDatabase() {
return database;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.Reference;
/**
* @author Christoph Strobl
* @since 3.3
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Reference
public @interface DocumentReference {
/**
* The database the referred entity resides in.
*
* @return empty String by default.
*/
String db() default "";
String collection() default "";
String lookup() default "{ '_id' : ?#{#target} }";
String sort() default "";
boolean lazy() default false;
}

View File

@@ -0,0 +1,24 @@
/*
* 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.mapping;
/**
* @author Christoph Strobl
*/
@FunctionalInterface
public interface ObjectReference<T> {
T getPointer();
}

View File

@@ -0,0 +1,649 @@
/*
* 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;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
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.DocumentReference;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.ObjectReference;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.model.Filters;
/**
* {@link DBRef} related integration tests for {@link MongoTemplate}.
*
* @author Christoph Strobl
*/
@ExtendWith(MongoClientExtension.class)
public class MongoTemplateDocumentReferenceTests {
public static final String DB_NAME = "manual-reference-tests";
static @Client MongoClient client;
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
cfg.configureDatabaseFactory(it -> {
it.client(client);
it.defaultDb(DB_NAME);
});
cfg.configureConversion(it -> {
it.customConverters(new ReferencableConverter());
});
cfg.configureMappingContext(it -> {
it.autocreateIndex(false);
});
});
@BeforeEach
public void setUp() {
template.flushDatabase();
}
@Test
void writeSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
SingleRefRoot source = new SingleRefRoot();
source.id = "root-1";
source.simpleValueRef = new SimpleObjectRef("ref-1", "me-the-referenced-object");
template.save(source);
Document target = template.execute(db -> {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
});
assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
}
@Test
void writeMapTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
CollectionRefRoot source = new CollectionRefRoot();
source.id = "root-1";
source.mapValueRef = new LinkedHashMap<>();
source.mapValueRef.put("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"));
source.mapValueRef.put("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
template.save(source);
Document target = template.execute(db -> {
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
void writeCollectionOfSimpleTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
CollectionRefRoot source = new CollectionRefRoot();
source.id = "root-1";
source.simpleValueRef = Arrays.asList(new SimpleObjectRef("ref-1", "me-the-1-referenced-object"),
new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
template.save(source);
Document target = template.execute(db -> {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
});
assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
}
@Test
void writeObjectTypeReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
SingleRefRoot source = new SingleRefRoot();
source.id = "root-1";
source.objectValueRef = new ObjectRefOfDocument("ref-1", "me-the-referenced-object");
template.save(source);
Document target = template.execute(db -> {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
});
assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
}
@Test
void writeCollectionOfObjectTypeReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
CollectionRefRoot source = new CollectionRefRoot();
source.id = "root-1";
source.objectValueRef = Arrays.asList(new ObjectRefOfDocument("ref-1", "me-the-1-referenced-object"),
new ObjectRefOfDocument("ref-2", "me-the-2-referenced-object"));
template.save(source);
Document target = template.execute(db -> {
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
});
assertThat(target.get("objectValueRef", List.class)).containsExactly(
source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
}
@Test
void readSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef", "ref-1");
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
void readCollectionOfSimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
Collections.singletonList("ref-1"));
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-1", "me-the-referenced-object"));
}
@Test
void readLazySimpleTypeObjectReference() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleLazyValueRef", "ref-1");
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
LazyLoadingTestUtils.assertProxy(result.simpleLazyValueRef, (proxy) -> {
assertThat(proxy.isResolved()).isFalse();
assertThat(proxy.currentValue()).isNull();
});
assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
"ref-1");
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
assertThat(result.getSimpleValueRefWithAnnotatedFieldName())
.isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
Collections.singletonList("ref-1"));
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.getSimpleValueRefWithAnnotatedFieldName())
.containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
}
@Test
void readObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
new Document("id", "ref-1").append("property", "without-any-meaning"));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
}
@Test
void readCollectionObjectReferenceFromDocumentType() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
Collections.singletonList(new Document("id", "ref-1").append("property", "without-any-meaning")));
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.getObjectValueRef())
.containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
}
@Test
void readObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append(
"objectValueRefWithEmbeddedCollectionName",
new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
.append("property", "without-any-meaning"));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
assertThat(result.getObjectValueRefWithEmbeddedCollectionName())
.isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
}
@Test
void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
Document refSource1 = new Document("_id", "ref-1").append("value", "me-the-1-referenced-object");
Document refSource2 = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append(
"objectValueRefWithEmbeddedCollectionName",
Arrays.asList(
new Document("id", "ref-2").append("collection", "object-ref-of-document-with-embedded-collection-name"),
new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
.append("property", "without-any-meaning")));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource1);
db.getCollection(refCollectionName).insertOne(refSource2);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
assertThat(result.getObjectValueRefWithEmbeddedCollectionName()).containsExactly(
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-2", "me-the-2-referenced-object"),
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
}
@Test
void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
.append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
assertThat(result.getObjectValueRefOnNonIdFields())
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
.append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("lazyObjectValueRefOnNonIdFields",
new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
template.execute(db -> {
db.getCollection(refCollectionName).insertOne(refSource);
db.getCollection(rootCollectionName).insertOne(source);
return null;
});
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
LazyLoadingTestUtils.assertProxy(result.lazyObjectValueRefOnNonIdFields, (proxy) -> {
assertThat(proxy.isResolved()).isFalse();
assertThat(proxy.currentValue()).isNull();
});
assertThat(result.getLazyObjectValueRefOnNonIdFields())
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
.append("value", "me-the-referenced-object");
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
Collections.singletonList(new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property",
"without-any-meaning")));
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.getObjectValueRefOnNonIdFields())
.containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
}
@Test
void readMapOfReferences() {
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
Document refSource1 = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
.append("value", "me-the-1-referenced-object");
Document refSource2 = new Document("_id", "ref-2").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
.append("value", "me-the-2-referenced-object");
Map<String, String> refmap = new LinkedHashMap<>();
refmap.put("frodo", "ref-1");
refmap.put("bilbo", "ref-2");
Document source = new Document("_id", "id-1").append("value", "v1").append("mapValueRef", refmap);
template.execute(db -> {
db.getCollection(rootCollectionName).insertOne(source);
db.getCollection(refCollectionName).insertOne(refSource1);
db.getCollection(refCollectionName).insertOne(refSource2);
return null;
});
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"));
}
@Data
static class SingleRefRoot {
String id;
String value;
@DocumentReference SimpleObjectRefWithReadingConverter withReadingConverter;
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
SimpleObjectRef simpleValueRef;
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }", lazy = true) //
SimpleObjectRef simpleLazyValueRef;
@Field("simple-value-ref-annotated-field-name") //
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
SimpleObjectRef simpleValueRefWithAnnotatedFieldName;
@DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
ObjectRefOfDocument objectValueRef;
@DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "#collection") //
ObjectRefOfDocumentWithEmbeddedCollectionName objectValueRefWithEmbeddedCollectionName;
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
ObjectRefOnNonIdField objectValueRefOnNonIdFields;
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }", lazy = true) //
ObjectRefOnNonIdField lazyObjectValueRefOnNonIdFields;
}
@Data
static class CollectionRefRoot {
String id;
String value;
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
List<SimpleObjectRef> simpleValueRef;
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
Map<String, SimpleObjectRef> mapValueRef;
@Field("simple-value-ref-annotated-field-name") //
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
List<SimpleObjectRef> simpleValueRefWithAnnotatedFieldName;
@DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
List<ObjectRefOfDocument> objectValueRef;
@DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") //
List<ObjectRefOfDocumentWithEmbeddedCollectionName> objectValueRefWithEmbeddedCollectionName;
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
List<ObjectRefOnNonIdField> objectValueRefOnNonIdFields;
}
@FunctionalInterface
interface ReferenceAble {
Object toReference();
}
@Data
@AllArgsConstructor
@org.springframework.data.mongodb.core.mapping.Document("simple-object-ref")
static class SimpleObjectRef {
@Id String id;
String value;
}
@Getter
@Setter
static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) {
super(id, value);
}
}
@Data
@AllArgsConstructor
static class ObjectRefOfDocument implements ReferenceAble {
@Id String id;
String value;
@Override
public Object toReference() {
return new Document("id", id).append("property", "without-any-meaning");
}
}
@Data
@AllArgsConstructor
static class ObjectRefOfDocumentWithEmbeddedCollectionName implements ReferenceAble {
@Id String id;
String value;
@Override
public Object toReference() {
return new Document("id", id).append("collection", "object-ref-of-document-with-embedded-collection-name");
}
}
@Data
@AllArgsConstructor
static class ObjectRefOnNonIdField implements ReferenceAble {
@Id String id;
String value;
String refKey1;
String refKey2;
@Override
public Object toReference() {
return new Document("refKey1", refKey1).append("refKey2", refKey2);
}
}
static class ReferencableConverter implements Converter<ReferenceAble, ObjectReference> {
@Nullable
@Override
public ObjectReference 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;
}
@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);
}
}
@WritingConverter
class SimpleObjectRefWithReadingConverterToDocumentConverter
implements Converter<SimpleObjectRefWithReadingConverter, ObjectReference<Document>> {
@Nullable
@Override
public ObjectReference<Document> convert(SimpleObjectRefWithReadingConverter source) {
return () -> new Document("the-ref-key-you-did-not-expect", source.getId());
}
}
}

View File

@@ -115,6 +115,8 @@ public class DbRefMappingMongoConverterUnitTests {
when(dbMock.getCollection(anyString(), eq(Document.class))).thenReturn(collectionMock);
FindIterable fi = mock(FindIterable.class);
when(fi.limit(anyInt())).thenReturn(fi);
when(fi.sort(any())).thenReturn(fi);
when(fi.first()).thenReturn(mapValDocument);
when(collectionMock.find(Mockito.any(Bson.class))).thenReturn(fi);

View File

@@ -33,7 +33,6 @@ 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;
@@ -65,6 +64,8 @@ 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);
}
@@ -115,7 +116,7 @@ class DefaultDbRefResolverUnitTests {
DBRef ref1 = new DBRef("collection-1", o1.get("_id"));
DBRef ref2 = new DBRef("collection-1", o2.get("_id"));
when(cursorMock.into(any())).then(invocation -> Arrays.asList(o2, o1));
when(cursorMock.spliterator()).thenReturn(Arrays.asList(o2, o1).spliterator());
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(o1, o2);
}
@@ -128,7 +129,7 @@ class DefaultDbRefResolverUnitTests {
DBRef ref1 = new DBRef("collection-1", document.get("_id"));
DBRef ref2 = new DBRef("collection-1", document.get("_id"));
when(cursorMock.into(any())).then(invocation -> Arrays.asList(document));
when(cursorMock.spliterator()).thenReturn(Arrays.asList(document).spliterator());
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document);
}

View File

@@ -17,9 +17,12 @@ package org.springframework.data.mongodb.core.convert;
import static org.assertj.core.api.Assertions.*;
import java.util.function.Consumer;
import org.springframework.aop.framework.Advised;
import org.springframework.cglib.proxy.Factory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.LazyLoadingInterceptor;
import org.springframework.data.mongodb.core.mapping.Unwrapped;
import org.springframework.test.util.ReflectionTestUtils;
/**
@@ -49,8 +52,35 @@ public class LazyLoadingTestUtils {
}
}
public static void assertProxy(Object proxy, Consumer<LazyLoadingProxyValueRetriever> verification) {
LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor = (LazyLoadingProxyGenerator.LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice()
: ((Factory) proxy).getCallback(0));
verification.accept(new LazyLoadingProxyValueRetriever(interceptor));
}
private static LazyLoadingInterceptor extractInterceptor(Object proxy) {
return (LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice()
: ((Factory) proxy).getCallback(0));
}
public static class LazyLoadingProxyValueRetriever {
LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor;
public LazyLoadingProxyValueRetriever(LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor) {
this.interceptor = interceptor;
}
public boolean isResolved() {
return (boolean) ReflectionTestUtils.getField(interceptor, "resolved");
}
@Unwrapped.Nullable
public Object currentValue() {
return ReflectionTestUtils.getField(interceptor, "result");
}
}
}

View File

@@ -18,13 +18,21 @@ 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.ReferenceReader;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.text.DecimalFormat;
import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.Document;
import org.bson.types.ObjectId;
@@ -96,6 +104,13 @@ public class ReactivePerformanceTests {
context.afterPropertiesSet();
converter = new MappingMongoConverter(new DbRefResolver() {
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
return null;
}
@Override
public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler proxyHandler) {
@@ -117,6 +132,11 @@ public class ReactivePerformanceTests {
public List<Document> bulkFetch(List<DBRef> dbRefs) {
return null;
}
@Override
public ReferenceLoader getReferenceLoader() {
return null;
}
}, context);
operations = new ReactiveMongoTemplate(mongoDbFactory, converter);

View File

@@ -13,6 +13,7 @@
<logger name="org.springframework.data.mongodb.core" level="error"/>
<logger name="org.springframework.data.mongodb.test.util" level="info"/>
<!-- <logger name="org.springframework.data.mongodb.core.convert" level="trace"/>-->
<root level="error">
<appender-ref ref="console" />