diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java index da26f4cce..f482ae0f1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java @@ -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 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index 8b6674460..96b6c6876 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -46,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 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 result = mongoCollection // - .find(new Document("_id", new Document("$in", ids))) // - .into(new ArrayList<>()); + List 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 getCollection(ReferenceContext context) { + + return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + Document.class); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java new file mode 100644 index 000000000..27feca163 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java @@ -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 bulkFetch(ReferenceFilter filter, ReferenceContext context) { + + MongoCollection 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 getCollection(ReferenceContext context) { + + return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + Document.class); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java new file mode 100644 index 000000000..b4324b505 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java @@ -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> 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> 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(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java new file mode 100644 index 000000000..35da1e1e2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java @@ -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> 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> 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> 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); + } + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 74d189b4c..0d3378d39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -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 populateProperties(ConversionContext context, MongoPersistentEntity entity, - DocumentAccessor documentAccessor, - SpELExpressionEvaluator evaluator, S instance) { + DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) { PersistentPropertyAccessor 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) type.getType(), collectionName)); + maybeEmitEvent(new AfterLoadEvent<>(document, (Class) type.getType(), collectionName)); target = (T) readDocument(context, document, type); } @@ -1541,9 +1596,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @SuppressWarnings("ConstantConditions") - private T doConvert(Object value, Class target, @Nullable Class fallback) { + private T doConvert(Object value, Class target, + @Nullable Class 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java index 8cb28bfe1..cbd02ee74 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java @@ -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 handle() throws UnsupportedOperationException { throw new UnsupportedOperationException("DBRef resolution is not supported!"); } + + @Nullable + @Override + public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, + BiFunction> lookupFunction) { + return null; + } + + @Override + public ReferenceLoader getReferenceLoader() { + return handle(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java new file mode 100644 index 000000000..0bfd30d9b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java @@ -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 bulkFetch(ReferenceFilter filter, ReferenceContext context); + + interface ReferenceFilter { + + Bson getFilter(); + + default Bson getSort() { + return new Document(); + } + + default Stream apply(MongoCollection collection) { + return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false)); + } + + default Stream restoreOrder(Stream 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 apply(MongoCollection collection) { + + Document result = collection.find(getFilter()).sort(getSort()).limit(1).first(); + return result != null ? Stream.of(result) : Stream.empty(); + } + }; + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java new file mode 100644 index 000000000..84dfb9c38 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java @@ -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, MongoPersistentProperty>> mappingContext; + private final BiFunction documentConversionFunction; + private final Supplier spelContextSupplier; + + public ReferenceReader(MappingContext, MongoPersistentProperty> mappingContext, + BiFunction documentConversionFunction, + Supplier spelContextSupplier) { + + this(() -> mappingContext, documentConversionFunction, spelContextSupplier); + } + + public ReferenceReader( + Supplier, MongoPersistentProperty>> mappingContextSupplier, + BiFunction documentConversionFunction, + Supplier spelContextSupplier) { + + this.mappingContext = Lazy.of(mappingContextSupplier); + this.documentConversionFunction = documentConversionFunction; + this.spelContextSupplier = spelContextSupplier; + this.codec = new ParameterBindingDocumentCodec(); + } + + Object readReference(MongoPersistentProperty property, Object value, + BiFunction> lookupFunction) { + + SpELContext spELContext = spelContextSupplier.get(); + + ReferenceFilter filter = computeFilter(property, value, spELContext); + ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext); + + Stream 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 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 parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier 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 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 filterMap = new LinkedHashMap<>(); + + for (Entry entry : ((Map) 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 apply(MongoCollection 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 filterOrderMap; + + public MapReferenceFilter(Document filter, Document sort, Map 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 restoreOrder(Stream stream) { + + Map targetMap = new LinkedHashMap<>(); + List collected = stream.collect(Collectors.toList()); + + for (Entry filterMapping : filterOrderMap.entrySet()) { + + String key = filterMapping.getKey().toString(); + Optional 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 restoreOrder(Stream stream) { + + if (filter.containsKey("$or")) { + List 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 referenceList, Document document1, Document document2) { + + for (int i = 0; i < referenceList.size(); i++) { + + Set> entries = referenceList.get(i).entrySet(); + if (document1.entrySet().containsAll(entries)) { + return -1; + } + if (document2.entrySet().containsAll(entries)) { + return 1; + } + } + return referenceList.size(); + } + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java new file mode 100644 index 000000000..ff0895363 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java @@ -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> 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; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java new file mode 100644 index 000000000..d9af6ccee --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java @@ -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; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java new file mode 100644 index 000000000..ed787f66b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java @@ -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 getPointer(); +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java new file mode 100644 index 000000000..3cbc7fef7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java @@ -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 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 simpleValueRef; + + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") // + Map mapValueRef; + + @Field("simple-value-ref-annotated-field-name") // + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") // + List simpleValueRefWithAnnotatedFieldName; + + @DocumentReference(lookup = "{ '_id' : '?#{id}' }") // + List objectValueRef; + + @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") // + List objectValueRefWithEmbeddedCollectionName; + + @DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") // + List 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 { + + @Nullable + @Override + public ObjectReference convert(ReferenceAble source) { + return source::toReference; + } + } + + @WritingConverter + class DocumentToSimpleObjectRefWithReadingConverter + implements Converter, SimpleObjectRefWithReadingConverter> { + + private final MongoTemplate template; + + public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) { + this.template = template; + } + + @Nullable + @Override + public SimpleObjectRefWithReadingConverter convert(ObjectReference source) { + return template.findOne(query(where("id").is(source.getPointer().get("the-ref-key-you-did-not-expect"))), + SimpleObjectRefWithReadingConverter.class); + } + } + + @WritingConverter + class SimpleObjectRefWithReadingConverterToDocumentConverter + implements Converter> { + + @Nullable + @Override + public ObjectReference convert(SimpleObjectRefWithReadingConverter source) { + return () -> new Document("the-ref-key-you-did-not-expect", source.getId()); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index 2c0f8649e..84e7e2c2d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -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); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java index d7a287047..c0a6b8df9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverUnitTests.java @@ -33,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. 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); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java index 5006459fc..f5d43c8ef 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/LazyLoadingTestUtils.java @@ -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 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"); + } + + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java index e310d7d29..9aa1bb0b5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java @@ -18,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> lookupFunction) { + return null; + } + @Override public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler proxyHandler) { @@ -117,6 +132,11 @@ public class ReactivePerformanceTests { public List bulkFetch(List dbRefs) { return null; } + + @Override + public ReferenceLoader getReferenceLoader() { + return null; + } }, context); operations = new ReactiveMongoTemplate(mongoDbFactory, converter); diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index a36841c97..f15459086 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -13,6 +13,7 @@ +