From 7c44bf63ae48b3d9484a8b5f1ca24ac0f73305a3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 4 Mar 2021 14:32:09 +0100 Subject: [PATCH 1/6] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index a6d5da9170..c654cb4cca 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3602-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 0033bd11d5..b9a6fe423a 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3602-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index f62c8dc7f4..4d0818e799 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3602-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index c1efaea420..a591a20f8b 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3602-SNAPSHOT ../pom.xml From e208cef12938fa2cc5f8283a12a9430acb91442f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 5 Mar 2021 07:27:02 +0100 Subject: [PATCH 2/6] 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. --- .../mongodb/core/convert/DbRefResolver.java | 2 +- .../core/convert/DefaultDbRefResolver.java | 29 +- .../core/convert/DefaultReferenceLoader.java | 71 ++ .../convert/DefaultReferenceResolver.java | 69 ++ .../convert/LazyLoadingProxyGenerator.java | 253 +++++++ .../core/convert/MappingMongoConverter.java | 88 ++- .../core/convert/NoOpDbRefResolver.java | 16 + .../mongodb/core/convert/ReferenceLoader.java | 79 +++ .../mongodb/core/convert/ReferenceReader.java | 350 ++++++++++ .../core/convert/ReferenceResolver.java | 74 ++ .../core/mapping/DocumentReference.java | 50 ++ .../mongodb/core/mapping/ObjectReference.java | 24 + .../MongoTemplateDocumentReferenceTests.java | 649 ++++++++++++++++++ .../DbRefMappingMongoConverterUnitTests.java | 2 + .../DefaultDbRefResolverUnitTests.java | 7 +- .../core/convert/LazyLoadingTestUtils.java | 30 + .../performance/ReactivePerformanceTests.java | 20 + .../src/test/resources/logback.xml | 1 + 18 files changed, 1779 insertions(+), 35 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentReference.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java 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 da26f4cce6..f482ae0f1c 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 @@ * @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 8b6674460c..96b6c6876b 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.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 @@ * @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 Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr */ @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 List bulkFetch(List refs) { 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 @@ protected MongoCollection getCollection(DBRef dbref) { 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 0000000000..27feca163d --- /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 0000000000..b4324b505f --- /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 0000000000..35da1e1e23 --- /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 74d189b4c5..0d3378d39f 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.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.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 MappingMongoConverter(DbRefResolver dbRefResolver, 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 MappingMongoConverter(DbRefResolver dbRefResolver, 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 @@ private S read(ConversionContext context, MongoPersistentEnti } 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 @@ private Object readAndPopulateIdentifier(ConversionContext context, PersistentPr @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 @@ private Object readIdValue(ConversionContext context, SpELExpressionEvaluator ev 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 @@ private void readAssociation(Association association, P 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 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce 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 @@ protected List createCollection(Collection collection, MongoPersisten 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 @@ protected Bson createMap(Map map, MongoPersistentProperty proper 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 @@ protected Bson createMap(Map map, MongoPersistentProperty proper 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 @@ private List bulkReadAndConvertDBRefs(ConversionContext context, List(document, (Class) type.getType(), collectionName)); + maybeEmitEvent(new AfterLoadEvent<>(document, (Class) type.getType(), collectionName)); target = (T) readDocument(context, document, type); } @@ -1541,9 +1596,10 @@ private T doConvert(Object value, Class target) } @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 8cb28bfe14..cbd02ee74d 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 List bulkFetch(List dbRefs) { 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 0000000000..0bfd30d9b8 --- /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 0000000000..84dfb9c38f --- /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 0000000000..ff08953633 --- /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 0000000000..d9af6ccee1 --- /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 0000000000..ed787f66b4 --- /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 0000000000..3cbc7fef7f --- /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 2c0f8649e2..84e7e2c2d8 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 void convertDocumentWithMapDBRef() { 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 d7a2870477..c0a6b8df90 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.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 @@ void setUp() { 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 @@ void bulkFetchShouldRestoreOriginalOrder() { 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 @@ void bulkFetchContainsDuplicates() { 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 5006459fc8..f5d43c8ef0 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 @@ 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 static void assertProxyIsResolved(Object target, boolean expected) { } } + 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 e310d7d298..9aa1bb0b57 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 @@ 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 void setUp() throws Exception { 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 Document fetch(DBRef dbRef) { 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 a36841c97c..f154590864 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -13,6 +13,7 @@ + From 8c6505b46aa4670e5604ed61402c02da2b3c4239 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 May 2021 14:09:12 +0200 Subject: [PATCH 3/6] First pass of review and TODO comments. --- .../data/mongodb/core/convert/ReferenceLoader.java | 4 ++++ .../data/mongodb/core/convert/ReferenceReader.java | 5 ++++- .../data/mongodb/core/convert/ReferenceResolver.java | 1 + .../data/mongodb/core/mapping/ObjectReference.java | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) 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 index 0bfd30d9b8..184918529e 100644 --- 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 @@ -35,8 +35,10 @@ default Document fetch(ReferenceFilter filter, ReferenceContext context) { return bulkFetch(filter, context).findFirst().orElse(null); } + // meh, Stream! Stream bulkFetch(ReferenceFilter filter, ReferenceContext context); + // Reference query interface ReferenceFilter { Bson getFilter(); @@ -45,6 +47,8 @@ default Bson getSort() { return new Document(); } + // TODO: Move apply method into something else that holds the collection and knows about single item/multi-item + // processing default Stream apply(MongoCollection collection) { return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false)); } 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 index 84dfb9c38f..e5a16ea431 100644 --- 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 @@ -80,6 +80,7 @@ public ReferenceReader( this.codec = new ParameterBindingDocumentCodec(); } + // TODO: Move documentConversionFunction to here. Having a contextual read allows projections in references Object readReference(MongoPersistentProperty property, Object value, BiFunction> lookupFunction) { @@ -94,6 +95,8 @@ Object readReference(MongoPersistentProperty property, Object value, return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList()); } + // TODO: retain target type and extract types here so the conversion function doesn't require type fiddling + // BiFunction instead of MongoPersistentProperty if (property.isMap()) { // the order is a real problem here @@ -165,7 +168,7 @@ private T parseValueOrGet(String value, ParameterBindingContext bindingConte if (!BsonUtils.isJsonDocument(value) && value.contains("?#{")) { String s = "{ 'target-value' : " + value + "}"; - T evaluated = (T) new ParameterBindingDocumentCodec().decode(s, bindingContext).get("target-value "); + T evaluated = (T) codec.decode(s, bindingContext).get("target-value "); return evaluated != null ? evaluated : defaultValue.get(); } 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 index ff08953633..50bc6558d3 100644 --- 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 @@ -47,6 +47,7 @@ default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLoader getReferenceLoader(); + // TODO: ReferenceCollection class ReferenceContext { @Nullable final String database; 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 index ed787f66b4..9904b20d3f 100644 --- 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 @@ -19,6 +19,7 @@ * @author Christoph Strobl */ @FunctionalInterface +// TODO: ObjectPointer or DocumentPointer public interface ObjectReference { T getPointer(); } From 05c3a35c3588cd5d3017e1c9b967b212774e5cd8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 5 May 2021 09:44:53 +0200 Subject: [PATCH 4/6] Update entity linking support to derive document pointer from lookup query. Simplify usage by computing the pointer from the lookup. Update the reference documentation, add JavaDoc and refine API. --- .../core/convert/DefaultDbRefResolver.java | 15 +- .../core/convert/DefaultReferenceLoader.java | 14 +- .../convert/DefaultReferenceResolver.java | 22 +- .../core/convert/DocumentPointerFactory.java | 135 +++++ .../core/convert/LazyLoadingProxy.java | 11 + .../convert/LazyLoadingProxyGenerator.java | 30 +- .../core/convert/MappingMongoConverter.java | 108 +++- .../mongodb/core/convert/MongoWriter.java | 4 + .../core/convert/NoOpDbRefResolver.java | 7 +- .../mongodb/core/convert/QueryMapper.java | 12 +- .../mongodb/core/convert/ReferenceLoader.java | 38 +- .../mongodb/core/convert/ReferenceReader.java | 136 ++--- .../core/convert/ReferenceResolver.java | 45 +- .../mapping/BasicMongoPersistentProperty.java | 19 + ...ectReference.java => DocumentPointer.java} | 13 +- .../core/mapping/DocumentReference.java | 86 ++- .../core/mapping/MongoPersistentProperty.java | 19 + .../UnwrappedMongoPersistentProperty.java | 11 + .../MongoTemplateDocumentReferenceTests.java | 548 ++++++++++++++++-- .../DefaultDbRefResolverUnitTests.java | 7 +- .../core/convert/QueryMapperUnitTests.java | 29 + .../performance/ReactivePerformanceTests.java | 7 +- ...tractPersonRepositoryIntegrationTests.java | 15 + .../data/mongodb/repository/Person.java | 12 + .../mongodb/repository/PersonRepository.java | 2 + src/main/asciidoc/new-features.adoc | 5 + src/main/asciidoc/reference/mapping.adoc | 365 ++++++++++++ 27 files changed, 1468 insertions(+), 247 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/{ObjectReference.java => DocumentPointer.java} (57%) 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 96b6c6876b..5277fbc0b0 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,7 +46,7 @@ import org.springframework.data.mongodb.LazyLoadingException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisStd; @@ -117,7 +117,8 @@ public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr */ @Override public Document fetch(DBRef dbRef) { - return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef)); + return getReferenceLoader().fetch(DocumentReferenceQuery.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), + ReferenceCollection.fromDBRef(dbRef)); } /* @@ -157,9 +158,9 @@ public List bulkFetch(List refs) { databaseSource.getCollectionName()); } - List result = getReferenceLoader() - .bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next())) - .collect(Collectors.toList()); + List result = mongoCollection // + .find(new Document("_id", new Document("$in", ids))) // + .into(new ArrayList<>()); return ids.stream() // .flatMap(id -> documentWithId(id, result)) // @@ -498,9 +499,9 @@ protected MongoCollection getCollection(DBRef dbref) { .getCollection(dbref.getCollectionName(), Document.class); } - protected MongoCollection getCollection(ReferenceContext context) { + protected MongoCollection getCollection(ReferenceCollection context) { - return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(), 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 index 27feca163d..66b698077b 100644 --- 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 @@ -15,21 +15,15 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - import org.bson.Document; -import org.bson.conversions.Bson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; -import org.springframework.lang.Nullable; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; /** @@ -49,7 +43,7 @@ public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) { } @Override - public Stream bulkFetch(ReferenceFilter filter, ReferenceContext context) { + public Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context) { MongoCollection collection = getCollection(context); @@ -63,9 +57,9 @@ public Stream bulkFetch(ReferenceFilter filter, ReferenceContext conte return filter.apply(collection); } - protected MongoCollection getCollection(ReferenceContext context) { + protected MongoCollection getCollection(ReferenceCollection context) { - return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection, + return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(), 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 index b4324b505f..0692f719b5 100644 --- 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 @@ -15,12 +15,6 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.function.BiFunction; -import java.util.stream.Stream; - -import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; @@ -44,24 +38,26 @@ public ReferenceLoader getReferenceLoader() { @Nullable @Override public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { if (isLazyReference(property)) { - return createLazyLoadingProxy(property, source, referenceReader, lookupFunction); + return createLazyLoadingProxy(property, source, referenceReader, lookupFunction, resultConversionFunction); } - return referenceReader.readReference(property, source, lookupFunction); + return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); } private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - ReferenceReader referenceReader, BiFunction> lookupFunction) { - return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction); + ReferenceReader referenceReader, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { + return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction, + resultConversionFunction); } protected boolean isLazyReference(MongoPersistentProperty property) { - if (property.findAnnotation(DocumentReference.class) != null) { - return property.findAnnotation(DocumentReference.class).lazy(); + if (property.isDocumentReference()) { + return property.getDocumentReference().lazy(); } return property.getDBRef() != null && property.getDBRef().lazy(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java new file mode 100644 index 0000000000..a91a48d922 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.convert; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bson.Document; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; +import org.springframework.data.mongodb.core.mapping.DocumentPointer; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * @author Christoph Strobl + * @since 3.3 + */ +class DocumentPointerFactory { + + private ConversionService conversionService; + private MappingContext, MongoPersistentProperty> mappingContext; + private Map linkageMap; + + public DocumentPointerFactory(ConversionService conversionService, + MappingContext, MongoPersistentProperty> mappingContext) { + + this.conversionService = conversionService; + this.mappingContext = mappingContext; + this.linkageMap = new HashMap<>(); + } + + public DocumentPointer computePointer(MongoPersistentProperty property, Object value, Class typeHint) { + + if (value instanceof LazyLoadingProxy) { + return () -> ((LazyLoadingProxy) value).getSource(); + } + + if (conversionService.canConvert(typeHint, DocumentPointer.class)) { + return conversionService.convert(value, DocumentPointer.class); + } else { + + MongoPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(property.getAssociationTargetType()); + + if (!property.getDocumentReference().lookup().toLowerCase().replaceAll("\\s", "").replaceAll("'", "") + .equals("{_id:?#{#target}}")) { + + return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), key -> { + return new LinkageDocument(key); + }).get(persistentEntity, + BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value)); + } + + // just take the id as a reference + return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); + } + } + + static class LinkageDocument { + + String lookup; + org.bson.Document fetchDocument; + Map mapMap; + + public LinkageDocument(String lookup) { + + this.lookup = lookup; + String targetLookup = lookup; + + Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); + + Matcher matcher = pattern.matcher(lookup); + int index = 0; + mapMap = new LinkedHashMap<>(); + while (matcher.find()) { + + String expr = matcher.group(); + mapMap.put(Integer.valueOf(index), expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") + .replace("target.", "").replaceAll("'", "")); + targetLookup = targetLookup.replace(expr, index + ""); + index++; + } + + fetchDocument = org.bson.Document.parse(targetLookup); + } + + org.bson.Document get(MongoPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor) { + + org.bson.Document targetDocument = new Document(); + + // TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing? + // like we have it ordered by index values and could provide the parameter array from it. + + for (Entry entry : fetchDocument.entrySet()) { + + if (entry.getKey().equals("target")) { + + String refKey = mapMap.get(entry.getValue()); + + if (persistentEntity.hasIdProperty()) { + targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty())); + } else { + targetDocument.put(refKey, propertyAccessor.getBean()); + } + continue; + } + + Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey())); + String refKey = mapMap.get(entry.getValue()); + targetDocument.put(refKey, target); + } + return targetDocument; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java index a04a100cc5..8be7111988 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java @@ -46,4 +46,15 @@ public interface LazyLoadingProxy { */ @Nullable DBRef toDBRef(); + + /** + * Returns the raw {@literal source} object that defines the reference. + * + * @return can be {@literal null}. + * @since 3.3 + */ + @Nullable + default Object getSource() { + return toDBRef(); + } } 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 index 35da1e1e23..570a516d9b 100644 --- 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 @@ -19,23 +19,19 @@ import java.io.Serializable; import java.lang.reflect.Method; -import java.util.function.BiFunction; -import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.bson.Document; -import org.bson.conversions.Bson; import org.springframework.aop.framework.ProxyFactory; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.objenesis.ObjenesisStd; import org.springframework.util.ReflectionUtils; @@ -54,11 +50,12 @@ public LazyLoadingProxyGenerator(ReferenceReader referenceReader) { this.objenesis = new ObjenesisStd(true); } - public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - BiFunction> lookupFunction) { + public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { Class propertyType = property.getType(); - LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction); + LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction, + resultConversionFunction); if (!propertyType.isInterface()) { @@ -105,27 +102,30 @@ public static class LazyLoadingInterceptor private volatile boolean resolved; private @org.springframework.lang.Nullable Object result; private Object source; - private BiFunction> lookupFunction; + private LookupFunction lookupFunction; + private ResultConversionFunction resultConversionFunction; - private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD; + private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD, GET_SOURCE_METHOD; { try { INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget"); TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef"); FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize"); + GET_SOURCE_METHOD = LazyLoadingProxy.class.getMethod("getSource"); } catch (Exception e) { throw new RuntimeException(e); } } public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { this.property = property; this.source = source; this.referenceReader = reader; this.lookupFunction = lookupFunction; + this.resultConversionFunction = resultConversionFunction; } @Nullable @@ -145,6 +145,10 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox return null; } + if (GET_SOURCE_METHOD.equals(method)) { + return source; + } + if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) { if (ReflectionUtils.isToStringMethod(method)) { @@ -234,7 +238,7 @@ private synchronized Object resolve() { // property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()); // } - return referenceReader.readReference(property, source, lookupFunction); + return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); } catch (RuntimeException ex) { throw ex; 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 0d3378d39f..2ad4d75230 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 @@ -63,10 +63,9 @@ import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.core.mapping.DocumentReference; +import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.ObjectReference; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; @@ -124,6 +123,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; + private DocumentPointerFactory documentPointerFactory; /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -154,8 +154,8 @@ public MappingMongoConverter(DbRefResolver dbRefResolver, return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator); }); - this.referenceReader = new ReferenceReader(mappingContext, - (prop, document) -> this.read(prop.getActualType(), document), () -> spELContext); + this.referenceReader = new ReferenceReader(mappingContext, () -> spELContext); + this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext); } /** @@ -366,6 +366,14 @@ private S read(ConversionContext context, MongoPersistentEnti SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); DocumentAccessor documentAccessor = new DocumentAccessor(bson); + if (bson.get("_id") != null) { + + Object existing = context.getPath().getPathItem(bson.get("_id"), entity.getCollection(), entity.getType()); + if (existing != null) { + return (S) existing; + } + } + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); ParameterValueProvider provider = persistenceConstructor != null @@ -376,6 +384,7 @@ private S read(ConversionContext context, MongoPersistentEnti S instance = instantiator.createInstance(entity, provider); if (entity.requiresPropertyPopulation()) { + return populateProperties(context, entity, documentAccessor, evaluator, instance); } @@ -451,7 +460,8 @@ private void readProperties(ConversionContext context, MongoPersistentEntity callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + evaluator); continue; } @@ -478,7 +488,8 @@ private void readProperties(ConversionContext context, MongoPersistentEntity callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + evaluator); continue; } @@ -494,7 +505,8 @@ private DbRefResolverCallback getDbRefResolverCallback(ConversionContext context } private void readAssociation(Association association, PersistentPropertyAccessor accessor, - DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) { + DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback, + ConversionContext context, SpELExpressionEvaluator evaluator) { MongoPersistentProperty property = association.getInverse(); final Object value = documentAccessor.get(property); @@ -503,26 +515,32 @@ private void readAssociation(Association association, P return; } - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { // quite unusual but sounds like worth having? - if (conversionService.canConvert(ObjectReference.class, property.getActualType())) { + if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) { - // collection like special treatment - accessor.setProperty(property, conversionService.convert(new ObjectReference() { + DocumentPointer pointer = new DocumentPointer() { @Override public Object getPointer() { return value; } - }, property.getActualType())); + }; + + // collection like special treatment + accessor.setProperty(property, conversionService.convert(pointer, property.getActualType())); } else { - accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader)); + accessor.setProperty(property, + dbRefResolver.resolveReference(property, value, referenceReader, context::convert)); } return; } DBRef dbref = value instanceof DBRef ? (DBRef) value : null; + + // TODO: accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader, + // context::convert)); accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler)); } @@ -563,6 +581,45 @@ public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringP return createDBRef(object, referringProperty); } + public Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { + + if (source instanceof LazyLoadingProxy) { + return ((LazyLoadingProxy) source).getSource(); + } + + if (referringProperty != null) { + + if (referringProperty.isDbReference()) { + return toDBRef(source, referringProperty); + } + if (referringProperty.isDocumentReference()) { + return createDocumentPointer(source, referringProperty); + } + } + + throw new RuntimeException("oops - what's that " + source); + } + + Object createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { + + if (referringProperty == null) { + return source; + } + + if (ClassUtils.isAssignableValue(referringProperty.getType(), source) + && conversionService.canConvert(referringProperty.getType(), DocumentPointer.class)) { + return conversionService.convert(source, DocumentPointer.class).getPointer(); + } + + if (ClassUtils.isAssignableValue(referringProperty.getAssociationTargetType(), source)) { + return documentPointerFactory.computePointer(referringProperty, source, referringProperty.getActualType()) + .getPointer(); + + } + + return source; + } + /** * Root entry method into write conversion. Adds a type discriminator to the {@link Document}. Shouldn't be called for * nested conversions. @@ -749,13 +806,8 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce if (prop.isAssociation()) { - if (conversionService.canConvert(valueType.getType(), ObjectReference.class)) { - accessor.put(prop, conversionService.convert(obj, ObjectReference.class).getPointer()); - } else { - // just take the id as a reference - accessor.put(prop, mappingContext.getPersistentEntity(prop.getAssociationTargetType()) - .getIdentifierAccessor(obj).getIdentifier()); - } + accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext) + .computePointer(prop, obj, valueType.getType()).getPointer()); return; } @@ -799,14 +851,14 @@ protected List createCollection(Collection collection, MongoPersisten if (property.isAssociation()) { return writeCollectionInternal(collection.stream().map(it -> { - if (conversionService.canConvert(it.getClass(), ObjectReference.class)) { - return conversionService.convert(it, ObjectReference.class).getPointer(); + if (conversionService.canConvert(it.getClass(), DocumentPointer.class)) { + return conversionService.convert(it, DocumentPointer.class).getPointer(); } else { // just take the id as a reference return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it) .getIdentifier(); } - }).collect(Collectors.toList()), ClassTypeInformation.from(ObjectReference.class), new BasicDBList()); + }).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new BasicDBList()); } if (property.hasExplicitWriteTarget()) { @@ -855,15 +907,15 @@ protected Bson createMap(Map map, MongoPersistentProperty proper if (conversions.isSimpleType(key.getClass())) { String simpleKey = prepareMapKey(key.toString()); - if(property.isDbReference()) { + if (property.isDbReference()) { document.put(simpleKey, value != null ? createDBRef(value, property) : null); } else { - if (conversionService.canConvert(value.getClass(), ObjectReference.class)) { - document.put(simpleKey, conversionService.convert(value, ObjectReference.class).getPointer()); + if (conversionService.canConvert(value.getClass(), DocumentPointer.class)) { + document.put(simpleKey, conversionService.convert(value, DocumentPointer.class).getPointer()); } else { // just take the id as a reference - document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value) - .getIdentifier()); + document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()) + .getIdentifierAccessor(value).getIdentifier()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index 0f64177bca..779b3236d3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -70,4 +70,8 @@ default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity * @return will never be {@literal null}. */ DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty); + + default Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { + return toDBRef(source, referringProperty); + } } 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 cbd02ee74d..8b6c969439 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 @@ -20,9 +20,9 @@ import java.util.stream.Stream; import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -77,7 +77,8 @@ private T handle() throws UnsupportedOperationException { @Nullable @Override public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction) { + LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index af93fdd634..36353e4f86 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -605,7 +605,7 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers if (source instanceof Iterable) { BasicDBList result = new BasicDBList(); for (Object element : (Iterable) source) { - result.add(createDbRefFor(element, property)); + result.add(createReferenceFor(element, property)); } return result; } @@ -614,12 +614,12 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers Document result = new Document(); Document dbObject = (Document) source; for (String key : dbObject.keySet()) { - result.put(key, createDbRefFor(dbObject.get(key), property)); + result.put(key, createReferenceFor(dbObject.get(key), property)); } return result; } - return createDbRefFor(source, property); + return createReferenceFor(source, property); } /** @@ -666,12 +666,16 @@ private Entry createMapEntry(String key, @Nullable Object value) return Collections.singletonMap(key, value).entrySet().iterator().next(); } - private DBRef createDbRefFor(Object source, MongoPersistentProperty property) { + private Object createReferenceFor(Object source, MongoPersistentProperty property) { if (source instanceof DBRef) { return (DBRef) source; } + if(property != null && property.isDocumentReference()) { + return converter.toDocumentReference(source, property); + } + return converter.toDBRef(source, property); } 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 index 184918529e..d5c72afad8 100644 --- 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 @@ -15,12 +15,12 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.Collections; +import java.util.Iterator; import org.bson.Document; import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.lang.Nullable; import com.mongodb.client.MongoCollection; @@ -31,15 +31,15 @@ public interface ReferenceLoader { @Nullable - default Document fetch(ReferenceFilter filter, ReferenceContext context) { - return bulkFetch(filter, context).findFirst().orElse(null); + default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) { + + Iterator it = bulkFetch(filter, context).iterator(); + return it.hasNext() ? it.next() : null; } - // meh, Stream! - Stream bulkFetch(ReferenceFilter filter, ReferenceContext context); + Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context); - // Reference query - interface ReferenceFilter { + interface DocumentReferenceQuery { Bson getFilter(); @@ -49,21 +49,21 @@ default Bson getSort() { // TODO: Move apply method into something else that holds the collection and knows about single item/multi-item // processing - default Stream apply(MongoCollection collection) { - return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false)); + default Iterable apply(MongoCollection collection) { + return restoreOrder(collection.find(getFilter()).sort(getSort())); } - - default Stream restoreOrder(Stream stream) { - return stream; + + default Iterable restoreOrder(Iterable documents) { + return documents; } - static ReferenceFilter referenceFilter(Bson bson) { + static DocumentReferenceQuery referenceFilter(Bson bson) { return () -> bson; } - static ReferenceFilter singleReferenceFilter(Bson bson) { + static DocumentReferenceQuery singleReferenceFilter(Bson bson) { - return new ReferenceFilter() { + return new DocumentReferenceQuery() { @Override public Bson getFilter() { @@ -71,10 +71,10 @@ public Bson getFilter() { } @Override - public Stream apply(MongoCollection collection) { + public Iterable apply(MongoCollection collection) { Document result = collection.find(getFilter()).sort(getSort()).limit(1).first(); - return result != null ? Stream.of(result) : Stream.empty(); + return result != null ? Collections.singleton(result) : Collections.emptyList(); } }; } 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 index e5a16ea431..fb37367b1d 100644 --- 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 @@ -17,24 +17,24 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.bson.Document; import org.bson.conversions.Bson; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -56,61 +56,47 @@ */ public class ReferenceReader { - private final ParameterBindingDocumentCodec codec; - private final Lazy, MongoPersistentProperty>> mappingContext; - private final BiFunction documentConversionFunction; private final Supplier spelContextSupplier; + private final ParameterBindingDocumentCodec codec; public ReferenceReader(MappingContext, MongoPersistentProperty> mappingContext, - BiFunction documentConversionFunction, Supplier spelContextSupplier) { - this(() -> mappingContext, documentConversionFunction, spelContextSupplier); + this(() -> mappingContext, 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(); } - // TODO: Move documentConversionFunction to here. Having a contextual read allows projections in references - Object readReference(MongoPersistentProperty property, Object value, - BiFunction> lookupFunction) { + Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, + ResultConversionFunction resultConversionFunction) { SpELContext spELContext = spelContextSupplier.get(); - ReferenceFilter filter = computeFilter(property, value, spELContext); - ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext); + DocumentReferenceQuery filter = computeFilter(property, value, spELContext); + ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext); - Stream result = lookupFunction.apply(referenceContext, filter); + Iterable result = lookupFunction.apply(filter, referenceCollection); - if (property.isCollectionLike()) { - return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList()); + if (!result.iterator().hasNext()) { + return null; } - // TODO: retain target type and extract types here so the conversion function doesn't require type fiddling - // BiFunction instead of MongoPersistentProperty - 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); + if (property.isCollectionLike()) { + return resultConversionFunction.apply(result, property.getTypeInformation()); } - return result.map(it -> documentConversionFunction.apply(property, it)).findFirst().orElse(null); + return resultConversionFunction.apply(result.iterator().next(), property.getTypeInformation()); } - private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value, + private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, SpELContext spELContext) { if (value instanceof Iterable) { @@ -118,44 +104,43 @@ private ReferenceContext computeReferenceContext(MongoPersistentProperty propert } if (value instanceof DBRef) { - return ReferenceContext.fromDBRef((DBRef) value); + return ReferenceCollection.fromDBRef((DBRef) value); } if (value instanceof Document) { Document ref = (Document) value; - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> ref.get("db", String.class)); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> ref.get("collection", mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); - return new ReferenceContext(targetDatabase, targetCollection); + return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceContext(ref.getString("db"), ref.get("collection", + return new ReferenceCollection(ref.getString("db"), ref.get("collection", mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); } - if (property.isAnnotationPresent(DocumentReference.class)) { + if (property.isDocumentReference()) { ParameterBindingContext bindingContext = bindingContext(property, value, spELContext); - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); - Document sort = parseValueOrGet(documentReference.sort(), bindingContext, () -> null); - return new ReferenceContext(targetDatabase, targetCollection); + return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceContext(null, + return new ReferenceCollection(null, mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); } @@ -201,9 +186,9 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object return ctx; } - ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { + DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { - DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class); + DocumentReference documentReference = property.getDocumentReference(); String lookup = documentReference.lookup(); Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null); @@ -217,7 +202,7 @@ ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, Sp ors.add(decoded); } - return new ListReferenceFilter(new Document("$or", ors), sort); + return new ListDocumentReferenceQuery(new Document("$or", ors), sort); } if (property.isMap() && value instanceof Map) { @@ -230,18 +215,18 @@ ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, Sp filterMap.put(entry.getKey(), decoded); } - return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap); + return new MapDocumentReferenceQuery(new Document("$or", filterMap.values()), sort, filterMap); } - return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); + return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); } - static class SingleReferenceFilter implements ReferenceFilter { + static class SingleDocumentReferenceQuery implements DocumentReferenceQuery { Document filter; Document sort; - public SingleReferenceFilter(Document filter, Document sort) { + public SingleDocumentReferenceQuery(Document filter, Document sort) { this.filter = filter; this.sort = sort; } @@ -252,24 +237,24 @@ public Bson getFilter() { } @Override - public Stream apply(MongoCollection collection) { + public Iterable apply(MongoCollection collection) { Document result = collection.find(getFilter()).limit(1).first(); - return result != null ? Stream.of(result) : Stream.empty(); + return result != null ? Collections.singleton(result) : Collections.emptyList(); } } - static class MapReferenceFilter implements ReferenceFilter { + static class MapDocumentReferenceQuery implements DocumentReferenceQuery { - Document filter; - Document sort; - Map filterOrderMap; + private final Document filter; + private final Document sort; + private final Map filterOrderMap; - public MapReferenceFilter(Document filter, Document sort, Map filterOrderMap) { + public MapDocumentReferenceQuery(Document filter, Document sort, Map filterOrderMap) { this.filter = filter; - this.filterOrderMap = filterOrderMap; this.sort = sort; + this.filterOrderMap = filterOrderMap; } @Override @@ -283,45 +268,46 @@ public Bson getSort() { } @Override - public Stream restoreOrder(Stream stream) { + public Iterable restoreOrder(Iterable documents) { Map targetMap = new LinkedHashMap<>(); - List collected = stream.collect(Collectors.toList()); + List collected = documents instanceof List ? (List) documents + : Streamable.of(documents).toList(); for (Entry filterMapping : filterOrderMap.entrySet()) { - String key = filterMapping.getKey().toString(); - Optional first = collected.stream().filter(it -> { + Optional first = collected.stream() + .filter(it -> it.entrySet().containsAll(filterMapping.getValue().entrySet())).findFirst(); - boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet()); - return found; - }).findFirst(); - - targetMap.put(key, first.orElse(null)); + targetMap.put(filterMapping.getKey().toString(), first.orElse(null)); } - return Stream.of(new Document(targetMap)); + return Collections.singleton(new Document(targetMap)); } } - static class ListReferenceFilter implements ReferenceFilter { + static class ListDocumentReferenceQuery implements DocumentReferenceQuery { - Document filter; - Document sort; + private final Document filter; + private final Document sort; + + public ListDocumentReferenceQuery(Document filter, Document sort) { - public ListReferenceFilter(Document filter, Document sort) { this.filter = filter; this.sort = sort; } @Override - public Stream restoreOrder(Stream stream) { + public Iterable restoreOrder(Iterable documents) { if (filter.containsKey("$or")) { List ors = filter.get("$or", List.class); - return stream.sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)); + List target = documents instanceof List ? (List) documents + : Streamable.of(documents).toList(); + return target.stream().sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)) + .collect(Collectors.toList()); } - return stream; + return documents; } public Document getFilter() { @@ -347,7 +333,5 @@ int compareAgainstReferenceIndex(List referenceList, Document document } 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 index 50bc6558d3..f29dc16a7c 100644 --- 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 @@ -15,13 +15,12 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.function.BiFunction; -import java.util.stream.Stream; +import java.util.Collections; import org.bson.Document; -import org.bson.conversions.Bson; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -33,34 +32,38 @@ public interface ReferenceResolver { @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - BiFunction> lookupFunction); + LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction); - default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader) { - return resolveReference(property, source, referenceReader, (ctx, filter) -> { + default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, + ResultConversionFunction resultConversionFunction) { + + return resolveReference(property, source, referenceReader, (filter, ctx) -> { if (property.isCollectionLike() || property.isMap()) { return getReferenceLoader().bulkFetch(filter, ctx); + } + Object target = getReferenceLoader().fetch(filter, ctx); - return target == null ? Stream.empty() : Stream.of(getReferenceLoader().fetch(filter, ctx)); - }); + return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetch(filter, ctx)); + }, resultConversionFunction); } ReferenceLoader getReferenceLoader(); - // TODO: ReferenceCollection - class ReferenceContext { + class ReferenceCollection { - @Nullable final String database; - final String collection; + @Nullable + private final String database; + private final String collection; - public ReferenceContext(@Nullable String database, String collection) { + public ReferenceCollection(@Nullable String database, String collection) { this.database = database; this.collection = collection; } - static ReferenceContext fromDBRef(DBRef dbRef) { - return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName()); + static ReferenceCollection fromDBRef(DBRef dbRef) { + return new ReferenceCollection(dbRef.getDatabaseName(), dbRef.getCollectionName()); } public String getCollection() { @@ -72,4 +75,14 @@ public String getDatabase() { return database; } } + + @FunctionalInterface + interface LookupFunction { + Iterable apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection); + } + + @FunctionalInterface + interface ResultConversionFunction { + Object apply(Object source, TypeInformation property); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 0b47c79d04..b7b71a7fee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -231,6 +231,15 @@ public boolean isDbReference() { return isAnnotationPresent(DBRef.class); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isDocumentReference() + */ + @Override + public boolean isDocumentReference() { + return isAnnotationPresent(DocumentReference.class); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDBRef() @@ -240,6 +249,16 @@ public DBRef getDBRef() { return findAnnotation(DBRef.class); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#getDocumentReference() + */ + @Nullable + @Override + public DocumentReference getDocumentReference() { + return findAnnotation(DocumentReference.class); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty() 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/DocumentPointer.java similarity index 57% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ObjectReference.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DocumentPointer.java index 9904b20d3f..de7fbff866 100644 --- 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/DocumentPointer.java @@ -16,10 +16,19 @@ package org.springframework.data.mongodb.core.mapping; /** + * A custom pointer to a linked document to be used along with {@link DocumentReference} for storing the linkage value. + * * @author Christoph Strobl */ @FunctionalInterface -// TODO: ObjectPointer or DocumentPointer -public interface ObjectReference { +public interface DocumentPointer { + + /** + * The actual pointer value. This can be any simple type, like a {@link String} or {@link org.bson.types.ObjectId} or + * a {@link org.bson.Document} holding more information like the target collection, multiple fields forming the key, + * etc. + * + * @return the value stored in MongoDB and used for constructing the {@link DocumentReference#lookup() lookup query}. + */ T getPointer(); } 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 index d9af6ccee1..0846c4022c 100644 --- 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 @@ -24,8 +24,69 @@ import org.springframework.data.annotation.Reference; /** + * A {@link DocumentReference} offers an alternative way of linking entities in MongoDB. While the goal is the same as + * when using {@link DBRef}, the store representation is different and can be literally anything, a single value, an + * entire {@link org.bson.Document}, basically everything that can be stored in MongoDB. By default, the mapping layer + * will use the referenced entities {@literal id} value for storage and retrieval. + * + *
+ * public class Account {
+ *   private String id;
+ *   private Float total;
+ * }
+ *
+ * public class Person {
+ *   private String id;
+ *   @DocumentReference
+ *   private List<Account> accounts;
+ * }
+ * 
+ * Account account = ...
+ *
+ * mongoTemplate.insert(account);
+ *
+ * template.update(Person.class)
+ *   .matching(where("id").is(...))
+ *   .apply(new Update().push("accounts").value(account))
+ *   .first();
+ * 
+ * + * {@link #lookup()} allows to define custom queries that are independent from the {@literal id} field and in + * combination with {@link org.springframework.data.convert.WritingConverter writing converters} offer a flexible way of + * defining links between entities. + * + *
+ * public class Book {
+ * 	 private ObjectId id;
+ * 	 private String title;
+ *
+ * 	 @Field("publisher_ac")
+ * 	 @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }")
+ * 	 private Publisher publisher;
+ * }
+ *
+ * public class Publisher {
+ *
+ * 	 private ObjectId id;
+ * 	 private String acronym;
+ * 	 private String name;
+ *
+ * 	 @DocumentReference(lazy = true)
+ * 	 private List<Book> books;
+ * }
+ *
+ * @WritingConverter
+ * public class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {
+ *
+ *    public DocumentPointer<String> convert(Publisher source) {
+ * 		return () -> source.getAcronym();
+ *    }
+ * }
+ * 
+ * * @author Christoph Strobl * @since 3.3 + * @see MongoDB Reference Documentation */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -34,17 +95,38 @@ public @interface DocumentReference { /** - * The database the referred entity resides in. + * The database the linked entity resides in. * - * @return empty String by default. + * @return empty String by default. Uses the default database provided buy the {@link org.springframework.data.mongodb.MongoDatabaseFactory}. */ String db() default ""; + /** + * The database the linked entity resides in. + * + * @return empty String by default. Uses the property type for collection resolution. + */ String collection() default ""; + /** + * The single document lookup query. In case of an {@link java.util.Collection} or {@link java.util.Map} property + * the individual lookups are combined via an `$or` operator. + * + * @return an {@literal _id} based lookup. + */ String lookup() default "{ '_id' : ?#{#target} }"; + /** + * A specific sort. + * + * @return empty String by default. + */ String sort() default ""; + /** + * Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}. + * + * @return {@literal false} by default. + */ boolean lazy() default false; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java index 7c347229b6..c753f3856d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java @@ -62,6 +62,15 @@ public interface MongoPersistentProperty extends PersistentProperty { - it.customConverters(new ReferencableConverter()); + it.customConverters(new ReferencableConverter(), new SimpleObjectRefWithReadingConverterToDocumentConverter(), + new DocumentToSimpleObjectRefWithReadingConverter()); }); cfg.configureMappingContext(it -> { @@ -84,7 +87,7 @@ public void setUp() { template.flushDatabase(); } - @Test + @Test // GH-3602 void writeSimpleTypeReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -102,12 +105,11 @@ void writeSimpleTypeReference() { assertThat(target.get("simpleValueRef")).isEqualTo("ref-1"); } - @Test + @Test // GH-3602 void writeMapTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); - CollectionRefRoot source = new CollectionRefRoot(); source.id = "root-1"; source.mapValueRef = new LinkedHashMap<>(); @@ -120,11 +122,10 @@ void writeMapTypeReference() { return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); }); - System.out.println("target: " + target.toJson()); assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2"); } - @Test + @Test // GH-3602 void writeCollectionOfSimpleTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -143,7 +144,7 @@ void writeCollectionOfSimpleTypeReference() { assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2"); } - @Test + @Test // GH-3602 void writeObjectTypeReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -161,7 +162,7 @@ void writeObjectTypeReference() { assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference()); } - @Test + @Test // GH-3602 void writeCollectionOfObjectTypeReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -181,7 +182,7 @@ void writeCollectionOfObjectTypeReference() { source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference()); } - @Test + @Test // GH-3602 void readSimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -200,7 +201,7 @@ void readSimpleTypeObjectReference() { assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionOfSimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -220,7 +221,7 @@ void readCollectionOfSimpleTypeObjectReference() { assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readLazySimpleTypeObjectReference() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -245,7 +246,7 @@ void readLazySimpleTypeObjectReference() { assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readSimpleTypeObjectReferenceFromFieldWithCustomName() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -266,7 +267,7 @@ void readSimpleTypeObjectReferenceFromFieldWithCustomName() { .isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionTypeObjectReferenceFromFieldWithCustomName() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -287,7 +288,7 @@ void readCollectionTypeObjectReferenceFromFieldWithCustomName() { .containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentType() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -307,7 +308,7 @@ void readObjectReferenceFromDocumentType() { assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentType() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -328,7 +329,7 @@ void readCollectionObjectReferenceFromDocumentType() { .containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentDeclaringCollectionName() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -351,7 +352,7 @@ void readObjectReferenceFromDocumentDeclaringCollectionName() { .isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -379,7 +380,7 @@ void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() { new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object")); } - @Test + @Test // GH-3602 void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -401,7 +402,7 @@ void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() { .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(SingleRefRoot.class); @@ -429,7 +430,7 @@ void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() { .isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -452,7 +453,7 @@ void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() { .containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2")); } - @Test + @Test // GH-3602 void readMapOfReferences() { String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); @@ -479,12 +480,414 @@ void readMapOfReferences() { }); CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class); - System.out.println("result: " + result); - assertThat(result.getMapValueRef()).containsEntry("frodo", - new SimpleObjectRef("ref-1", "me-the-1-referenced-object")) - .containsEntry("bilbo", - new SimpleObjectRef("ref-2", "me-the-2-referenced-object")); + assertThat(result.getMapValueRef()) + .containsEntry("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object")) + .containsEntry("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object")); + } + + @Test // GH-3602 + void loadLazyCyclicReference() { + + WithRefA a = new WithRefA(); + a.id = "a"; + + WithRefB b = new WithRefB(); + b.id = "b"; + + a.toB = b; + b.lazyToA = a; + + template.save(a); + template.save(b); + + WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue(); + assertThat(loadedA).isNotNull(); + assertThat(loadedA.getToB()).isNotNull(); + LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> { + + assertThat(proxy.isResolved()).isFalse(); + assertThat(proxy.currentValue()).isNull(); + }); + } + + @Test // GH-3602 + void loadEagerCyclicReference() { + + WithRefA a = new WithRefA(); + a.id = "a"; + + WithRefB b = new WithRefB(); + b.id = "b"; + + a.toB = b; + b.eagerToA = a; + + template.save(a); + template.save(b); + + WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue(); + + assertThat(loadedA).isNotNull(); + assertThat(loadedA.getToB()).isNotNull(); + assertThat(loadedA.getToB().eagerToA).isSameAs(loadedA); + } + + @Test // GH-3602 + void loadAndStoreUnresolvedLazyDoesNotResolveTheProxy() { + + String collectionB = template.getCollectionName(WithRefB.class); + + WithRefA a = new WithRefA(); + a.id = "a"; + + WithRefB b = new WithRefB(); + b.id = "b"; + + a.toB = b; + b.lazyToA = a; + + template.save(a); + template.save(b); + + WithRefA loadedA = template.query(WithRefA.class).matching(where("id").is(a.id)).firstValue(); + template.save(loadedA.getToB()); + + LazyLoadingTestUtils.assertProxy(loadedA.getToB().lazyToA, (proxy) -> { + + assertThat(proxy.isResolved()).isFalse(); + assertThat(proxy.currentValue()).isNull(); + }); + + Document target = template.execute(db -> { + return db.getCollection(collectionB).find(Filters.eq("_id", "b")).first(); + }); + assertThat(target.get("lazyToA", Object.class)).isEqualTo("a"); + } + + @Test // GH-3602 + void loadCollectionReferenceWithMissingRefs() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + String refCollectionName = template.getCollectionName(SimpleObjectRef.class); + + // ref-1 is missing. + Document refSource = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object"); + Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef", + Arrays.asList("ref-1", "ref-2")); + + template.execute(db -> { + + db.getCollection(refCollectionName).insertOne(refSource); + db.getCollection(rootCollectionName).insertOne(source); + return null; + }); + + CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class); + assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-2", "me-the-2-referenced-object")); + } + + @Test // GH-3602 + void queryForReference() { + + WithRefB b = new WithRefB(); + b.id = "b"; + template.save(b); + + WithRefA a = new WithRefA(); + a.id = "a"; + a.toB = b; + template.save(a); + + WithRefA a2 = new WithRefA(); + a2.id = "a2"; + template.save(a2); + + WithRefA loadedA = template.query(WithRefA.class).matching(where("toB").is(b)).firstValue(); + assertThat(loadedA.getId()).isEqualTo(a.getId()); + } + + @Test // GH-3602 + void queryForReferenceInCollection() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + + Document shouldBeFound = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef", + Arrays.asList("ref-1", "ref-2")); + Document shouldNotBeFound = new Document("_id", "id-2").append("value", "v2").append("simpleValueRef", + Arrays.asList("ref-1")); + + template.execute(db -> { + + db.getCollection(rootCollectionName).insertOne(shouldBeFound); + db.getCollection(rootCollectionName).insertOne(shouldNotBeFound); + return null; + }); + + SimpleObjectRef objectRef = new SimpleObjectRef("ref-2", "some irrelevant value"); + + List loaded = template.query(CollectionRefRoot.class) + .matching(where("simpleValueRef").in(objectRef)).all(); + assertThat(loaded).map(CollectionRefRoot::getId).containsExactly("id-1"); + } + + @Test // GH-3602 + void queryForReferenceOnIdField() { + + WithRefB b = new WithRefB(); + b.id = "b"; + template.save(b); + + WithRefA a = new WithRefA(); + a.id = "a"; + a.toB = b; + template.save(a); + + WithRefA a2 = new WithRefA(); + a2.id = "a2"; + template.save(a2); + + WithRefA loadedA = template.query(WithRefA.class).matching(where("toB.id").is(b.id)).firstValue(); + assertThat(loadedA.getId()).isEqualTo(a.getId()); + } + + @Test // GH-3602 + void updateReferenceWithEntityHavingPointerConversion() { + + WithRefB b = new WithRefB(); + b.id = "b"; + template.save(b); + + WithRefA a = new WithRefA(); + a.id = "a"; + template.save(a); + + template.update(WithRefA.class).apply(new Update().set("toB", b)).first(); + + String collectionA = template.getCollectionName(WithRefA.class); + + Document target = template.execute(db -> { + return db.getCollection(collectionA).find(Filters.eq("_id", "a")).first(); + }); + + assertThat(target).containsEntry("toB", "b"); + } + + @Test // GH-3602 + void updateReferenceWithEntityWithoutPointerConversion() { + + String collectionName = template.getCollectionName(SingleRefRoot.class); + SingleRefRoot refRoot = new SingleRefRoot(); + refRoot.id = "root-1"; + + SimpleObjectRef ref = new SimpleObjectRef("ref-1", "me the referenced object"); + + template.save(refRoot); + + template.update(SingleRefRoot.class).apply(new Update().set("simpleValueRef", ref)).first(); + + Document target = template.execute(db -> { + return db.getCollection(collectionName).find(Filters.eq("_id", "root-1")).first(); + }); + + assertThat(target).containsEntry("simpleValueRef", "ref-1"); + } + + @Test // GH-3602 + void updateReferenceWithValue() { + + WithRefA a = new WithRefA(); + a.id = "a"; + template.save(a); + + template.update(WithRefA.class).apply(new Update().set("toB", "b")).first(); + + String collectionA = template.getCollectionName(WithRefA.class); + + Document target = template.execute(db -> { + return db.getCollection(collectionA).find(Filters.eq("_id", "a")).first(); + }); + + assertThat(target).containsEntry("toB", "b"); + } + + @Test // GH-3602 + void updateReferenceCollectionWithEntity() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + + CollectionRefRoot root = new CollectionRefRoot(); + root.id = "root-1"; + root.simpleValueRef = Collections.singletonList(new SimpleObjectRef("ref-1", "beastie")); + + template.save(root); + + template.update(CollectionRefRoot.class) + .apply(new Update().push("simpleValueRef").value(new SimpleObjectRef("ref-2", "boys"))).first(); + + Document target = template.execute(db -> { + return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); + }); + + assertThat(target).containsEntry("simpleValueRef", Arrays.asList("ref-1", "ref-2")); + } + + @Test // GH-3602 + void updateReferenceCollectionWithValue() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + + CollectionRefRoot root = new CollectionRefRoot(); + root.id = "root-1"; + root.simpleValueRef = Collections.singletonList(new SimpleObjectRef("ref-1", "beastie")); + + template.save(root); + + template.update(CollectionRefRoot.class).apply(new Update().push("simpleValueRef").value("ref-2")).first(); + + Document target = template.execute(db -> { + return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); + }); + + assertThat(target).containsEntry("simpleValueRef", Arrays.asList("ref-1", "ref-2")); + } + + @Test // GH-3602 + @Disabled("Property path resolution does not work inside maps, the key is considered :/") + void updateReferenceMapWithEntity() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + + CollectionRefRoot root = new CollectionRefRoot(); + root.id = "root-1"; + root.mapValueRef = Collections.singletonMap("beastie", new SimpleObjectRef("ref-1", "boys")); + + template.save(root); + + template.update(CollectionRefRoot.class) + .apply(new Update().set("mapValueRef.rise", new SimpleObjectRef("ref-2", "against"))).first(); + + Document target = template.execute(db -> { + return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); + }); + + assertThat(target).containsEntry("mapValueRef", new Document("beastie", "ref-1").append("rise", "ref-2")); + } + + @Test // GH-3602 + void updateReferenceMapWithValue() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + + CollectionRefRoot root = new CollectionRefRoot(); + root.id = "root-1"; + root.mapValueRef = Collections.singletonMap("beastie", new SimpleObjectRef("ref-1", "boys")); + + template.save(root); + + template.update(CollectionRefRoot.class).apply(new Update().set("mapValueRef.rise", "ref-2")).first(); + + Document target = template.execute(db -> { + return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first(); + }); + + assertThat(target).containsEntry("mapValueRef", new Document("beastie", "ref-1").append("rise", "ref-2")); + } + + @Test // GH-3602 + void useReadingWriterConverterPairForLoading() { + + SingleRefRoot root = new SingleRefRoot(); + root.id = "root-1"; + root.withReadingConverter = new SimpleObjectRefWithReadingConverter("ref-1", "value-1"); + + template.save(root.withReadingConverter); + + template.save(root); + + Document target = template.execute(db -> { + return db.getCollection(template.getCollectionName(SingleRefRoot.class)).find(Filters.eq("_id", root.id)).first(); + }); + + assertThat(target).containsEntry("withReadingConverter", + new Document("ref-key-from-custom-write-converter", root.withReadingConverter.id)); + + SingleRefRoot loaded = template.findOne(query(where("id").is(root.id)), SingleRefRoot.class); + assertThat(loaded.withReadingConverter).isInstanceOf(SimpleObjectRefWithReadingConverter.class); + } + + @Test // GH-3602 + void deriveMappingFromLookup() { + + Publisher publisher = new Publisher(); + publisher.id = "p-1"; + publisher.acronym = "TOR"; + publisher.name = "Tom Doherty Associates"; + + template.save(publisher); + + Book book = new Book(); + book.id = "book-1"; + book.publisher = publisher; + + template.save(book); + + Document target = template.execute(db -> { + return db.getCollection(template.getCollectionName(Book.class)).find(Filters.eq("_id", book.id)).first(); + }); + + assertThat(target).containsEntry("publisher", new Document("acc", publisher.acronym).append("n", publisher.name)); + + Book result = template.findOne(query(where("id").is(book.id)), Book.class); + assertThat(result.publisher).isNotNull(); + } + + @Test // GH-3602 + void updateDerivedMappingFromLookup() { + + Publisher publisher = new Publisher(); + publisher.id = "p-1"; + publisher.acronym = "TOR"; + publisher.name = "Tom Doherty Associates"; + + template.save(publisher); + + Book book = new Book(); + book.id = "book-1"; + + template.save(book); + + template.update(Book.class).matching(where("id").is(book.id)).apply(new Update().set("publisher", publisher)).first(); + + Document target = template.execute(db -> { + return db.getCollection(template.getCollectionName(Book.class)).find(Filters.eq("_id", book.id)).first(); + }); + + assertThat(target).containsEntry("publisher", new Document("acc", publisher.acronym).append("n", publisher.name)); + + Book result = template.findOne(query(where("id").is(book.id)), Book.class); + assertThat(result.publisher).isNotNull(); + } + + @Test // GH-3602 + void queryDerivedMappingFromLookup() { + + Publisher publisher = new Publisher(); + publisher.id = "p-1"; + publisher.acronym = "TOR"; + publisher.name = "Tom Doherty Associates"; + + template.save(publisher); + + Book book = new Book(); + book.id = "book-1"; + book.publisher = publisher; + + template.save(book); + book.publisher = publisher; + + Book result = template.findOne(query(where("publisher").is(publisher)), Book.class); + assertThat(result.publisher).isNotNull(); } @Data @@ -556,16 +959,16 @@ 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) { + public SimpleObjectRefWithReadingConverter(String id, String value) { super(id, value); } + } @Data @@ -609,41 +1012,94 @@ public Object toReference() { } } - static class ReferencableConverter implements Converter { + static class ReferencableConverter implements Converter { @Nullable @Override - public ObjectReference convert(ReferenceAble source) { + public DocumentPointer convert(ReferenceAble source) { return source::toReference; } } @WritingConverter class DocumentToSimpleObjectRefWithReadingConverter - implements Converter, SimpleObjectRefWithReadingConverter> { + implements Converter, SimpleObjectRefWithReadingConverter> { - private final MongoTemplate template; + @Nullable + @Override + public SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { - public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) { - this.template = template; + Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref") + .find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first(); + return new SimpleObjectRefWithReadingConverter(document.getString("_id"), document.getString("value")); } + } + + @WritingConverter + class SimpleObjectRefWithReadingConverterToDocumentConverter + implements Converter> { @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); + public DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { + return () -> new Document("ref-key-from-custom-write-converter", source.getId()); } } - @WritingConverter - class SimpleObjectRefWithReadingConverterToDocumentConverter - implements Converter> { + @Getter + @Setter + static class WithRefA/* to B */ implements ReferenceAble { + + @Id String id; + @DocumentReference WithRefB toB; + + @Override + public Object toReference() { + return id; + } + } + + @Getter + @Setter + @ToString + static class WithRefB/* to A */ implements ReferenceAble { + + @Id String id; + @DocumentReference(lazy = true) WithRefA lazyToA; + + @DocumentReference WithRefA eagerToA; + + @Override + public Object toReference() { + return id; + } + } + + static class ReferencedObject {} + + class ToDocumentPointerConverter implements Converter> { @Nullable @Override - public ObjectReference convert(SimpleObjectRefWithReadingConverter source) { - return () -> new Document("the-ref-key-you-did-not-expect", source.getId()); + public DocumentPointer convert(ReferencedObject source) { + return () -> new Document("", source); } } + + @Data + static class Book { + + String id; + + @DocumentReference(lookup = "{ 'acronym' : ?#{acc}, 'name' : ?#{n} }") Publisher publisher; + + } + + static class Publisher { + + String id; + String acronym; + String name; + } + } 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 c0a6b8df90..d7a2870477 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,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -64,8 +65,6 @@ void setUp() { 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); } @@ -116,7 +115,7 @@ void bulkFetchShouldRestoreOriginalOrder() { DBRef ref1 = new DBRef("collection-1", o1.get("_id")); DBRef ref2 = new DBRef("collection-1", o2.get("_id")); - when(cursorMock.spliterator()).thenReturn(Arrays.asList(o2, o1).spliterator()); + when(cursorMock.into(any())).then(invocation -> Arrays.asList(o2, o1)); assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(o1, o2); } @@ -129,7 +128,7 @@ void bulkFetchContainsDuplicates() { DBRef ref1 = new DBRef("collection-1", document.get("_id")); DBRef ref2 = new DBRef("collection-1", document.get("_id")); - when(cursorMock.spliterator()).thenReturn(Arrays.asList(document).spliterator()); + when(cursorMock.into(any())).then(invocation -> Arrays.asList(document)); assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index e2f69260b1..9c157db759 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -48,6 +48,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -1487,4 +1488,32 @@ static class WithPropertyUsingUnderscoreInName { @Field("renamed") String renamed_fieldname_with_underscores; } + + static class WithDocumentReferences { + + @DocumentReference + Sample sample; + + @DocumentReference + SimpeEntityWithoutId noId; + + @DocumentReference(lookup = "{ 'stringProperty' : ?#{stringProperty} }") + SimpeEntityWithoutId noIdButLookupQuery; + + } + + @Test + void xxx() { + + Sample sample = new Sample(); + sample.foo = "sample-id"; + + Query query = query(where("sample").is(sample)); + + org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), + context.getPersistentEntity(WithDocumentReferences.class)); + + System.out.println("mappedObject.toJson(): " + mappedObject.toJson()); + } + } 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 9aa1bb0b57..b70930dae0 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,11 +18,10 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; -import org.bson.conversions.Bson; import org.springframework.data.mongodb.core.convert.ReferenceLoader; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.convert.ReferenceReader; -import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -107,7 +106,7 @@ public void setUp() throws Exception { @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction> lookupFunction) { + public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { return null; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 9ab37e3ff5..61caa30560 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1434,4 +1434,19 @@ void annotatedQueryShouldAllowAggregationInProjection() { Person target = repository.findWithAggregationInProjection(alicia.getId()); assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase()); } + + @Test // GH-3602 + void executesQueryWithDocumentReferenceCorrectly() { + + Person josh = new Person("Josh", "Long"); + User dave = new User(); + dave.id = "dave"; + + josh.setSpiritAnimal(dave); + + operations.save(josh); + + List result = repository.findBySpiritAnimal(dave); + assertThat(result).map(Person::getId).containsExactly(josh.getId()); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 01b0c28de2..62c5b18be5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Unwrapped; @@ -74,6 +75,9 @@ public enum Sex { @Unwrapped.Nullable(prefix = "u") // User unwrappedUser; + @DocumentReference + User spiritAnimal; + public Person() { this(null, null); @@ -308,6 +312,14 @@ public void setUnwrappedUser(User unwrappedUser) { this.unwrappedUser = unwrappedUser; } + public User getSpiritAnimal() { + return spiritAnimal; + } + + public void setSpiritAnimal(User spiritAnimal) { + this.spiritAnimal = spiritAnimal; + } + /* * (non-Javadoc) * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 314655e781..ca382fa2ca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -416,4 +416,6 @@ Person findPersonByManyArguments(String firstname, String lastname, String email List findByUnwrappedUserUsername(String username); List findByUnwrappedUser(User user); + + List findBySpiritAnimal(User user); } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 03d18bacf9..842dd8341b 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New & Noteworthy +[[new-features.3.3]] +== What's New in Spring Data MongoDB 3.3 + +* Extended support for <> entities. + [[new-features.3.2]] == What's New in Spring Data MongoDB 3.2 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 82b5632f2b..1998fe1ad8 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -480,6 +480,7 @@ The MappingMongoConverter can use metadata to drive the mapping of objects to do * `@MongoId`: Applied at the field level to mark the field used for identity purpose. Accepts an optional `FieldType` to customize id conversion. * `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the collection where the data will be stored. * `@DBRef`: Applied at the field to indicate it is to be stored using a com.mongodb.DBRef. +* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. This can be a single value (the _id_ by default), or a `Document` provided via a converter. * `@Indexed`: Applied at the field level to describe how to index the field. * `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes. * `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field. @@ -826,6 +827,370 @@ Required properties that are also defined as lazy loading ``DBRef`` and used as TIP: Lazily loaded ``DBRef``s can be hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()` or some inline debug rendering invoking property getters. Please consider to enable _trace_ logging for `org.springframework.data.mongodb.core.convert.DefaultDbRefResolver` to gain insight on `DBRef` resolution. +[[mapping-usage.linking]] +=== Using Document References + +Using `@DocumentReference` offers an alternative way of linking entities in MongoDB. +While the goal is the same as when using <>, the store representation is different. +`DBRef` resolves to a document with a fixed structure as outlined in the https://docs.mongodb.com/manual/reference/database-references/[MongoDB Reference documentation]. + +Document references, do not follow a specific format. +They can be literally anything, a single value, an entire document, basically everything that can be stored in MongoDB. +By default, the mapping layer will use the referenced entities _id_ value for storage and retrieval, like in the sample below. + +==== +[source,java] +---- +@Document +public class Account { + + @Id + private String id; + private Float total; +} + +@Document +public class Person { + + @Id + private String id; + + @DocumentReference <1> + private List accounts; +} +---- +[source,java] +---- +Account account = ... + +tempate.insert(account); <2> + +template.update(Person.class) + .matching(where("id").is(...)) + .apply(new Update().push("accounts").value(account)) <3> + .first(); +---- +[source,json] +---- +{ + "_id" : ..., + "accounts" : [ "6509b9e", ... ] <4> +} +---- +<1> Mark the collection of `Account` values to be linked. +<2> The mapping framework does not handle cascading saves, so make sure to persist the referenced entity individually. +<3> Add the reference to the existing entity. +<4> Linked `Account` entities are represented as an array of their `_id` values. +==== + +The sample above uses an `_id` based fetch query (`{ '_id' : ?#{#target} }`) for data retrieval and resolves linked entities eagerly. +It is possible to alter resolution defaults (listed below) via the attributes of `@DocumentReference` + +.@DocumentReference defaults +[cols="2,3,5", options="header"] +|=== +| Attribute | Description | Default + +| `db` +| The target database name for collection lookup. +| The configured database provided by `MongoDatabaseFactory.getMongoDatabase()`. + +| `collection` +| The target collection name. +| The annotated properties domain type, respectively the value type in case of `Collection` like or `Map` properties, collection name. + +| `lookup` +| The single document lookup query evaluating placeholders via SpEL expressions using `#target` as the marker for a given source value. `Collection` like or `Map` properties combine individual lookups via an `$or` operator. +| An `_id` field based query (`{ '_id' : ?#{#target} }`) using the loaded source value. + +| `lazy` +| If set to `true` value resolution is delayed upon first access of the property. +| Resolves properties eagerly by default. +|=== + +`@DocumentReference(lookup=...)` allows to define custom queries that are independent from the `_id` field and therefore offer a flexible way of defining links between entities as demonstrated in the sample below, where the `Publisher` of a book is referenced by its acronym instead of the internal `id`. + +==== +[source,java] +---- +@Document +public class Book { + + @Id + private ObjectId id; + private String title; + private List author; + + @Field("publisher_ac") + @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }") <1> + private Publisher publisher; +} + +@Document +public class Publisher { + + @Id + private ObjectId id; + private String acronym; <1> + private String name; + + @DocumentReference(lazy = true) <2> + private List books; + +} +---- +[source,json] +---- +{ + "_id" : 9a48e32, + "title" : "The Warded Man", + "author" : ["Peter V. Brett"], + "publisher_ac" : "DR" +} +---- +<1> Use the `acronym` field to query for entities in the `Publisher` collection. +<2> Lazy load back references to the `Book` collection. +==== + +The above snipped shows the reading side of things when working with custom linked objects. +To make the writing part aware of the modified document pointer a custom converter, capable of the transformation into a `DocumentPointer`, like the one below, needs to be registered. + +==== +[source,java] +---- +@WritingConverter +class PublisherReferenceConverter implements Converter> { + + @Override + public DocumentPointer convert(Publisher source) { + return () -> source.getAcronym(); + } +} +---- +==== + +If no `DocumentPointer` converter is provided the target linkage document can be computed based on the given lookup query. +In this case the association target properties are evaluated as shown in the following sample. + +==== +[source,java] +---- +@Document +public class Book { + + @Id + private ObjectId id; + private String title; + private List author; + + @DocumentReference(lookup = "{ 'acronym' : ?#{acc} }") <1> <2> + private Publisher publisher; +} + +@Document +public class Publisher { + + @Id + private ObjectId id; + private String acronym; <1> + private String name; + + // ... +} +---- +[source,json] +---- +{ + "_id" : 9a48e32, + "title" : "The Warded Man", + "author" : ["Peter V. Brett"], + "publisher" : { + "acc" : "DOC" + } +} +---- +<1> Use the `acronym` field to query for entities in the `Publisher` collection. +<2> The field value placeholders of the lookup query (like `acc`) is used to form the linkage document. +==== + +With all the above in place it is possible to model all kind of associations between entities. +Have a look at the non exhaustive list of samples below to get feeling for what is possible. + +.Simple Document Reference using _id_ field +==== +[source,java] +---- +class Entity { + @DocumentReference + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : "9a48e32" <1> +} + +// referenced object +{ + "_id" : "9a48e32" <1> +} +---- +<1> MongoDB simple type can be directly used without further configuration. +==== + +.Simple Document Reference using _id_ field with explicit lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") <1> + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : "9a48e32" <1> +} + +// referenced object +{ + "_id" : "9a48e32" +} +---- +<1> _target_ defines the linkage value itself. +==== + +.Document Reference extracting field of linkage document for lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{refKey}' }") <1> <2> + private ReferencedObject ref; +} +---- + +[source,java] +---- +@WritingConverter +class ToDocumentPointerConverter implements Converter> { + public DocumentPointer convert(ReferencedObject source) { + return () -> new Document("refKey", source.id); <1> + } +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "refKey" : "9a48e32" <1> + } +} + +// referenced object +{ + "_id" : "9a48e32" +} +---- +<1> The key used for obtaining the linkage value must be the one used during write. +<2> `refKey` is short for `target.refKey`. +==== + +.Document Reference with multiple values forming the lookup query +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ 'firstname' : '?#{fn}', 'lastname' : '?#{ln}' }") <1> <2> + private ReferencedObject ref; +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "fn" : "Josh", <1> + "ln" : "Long" <1> + } +} + +// referenced object +{ + "_id" : "9a48e32", + "firsntame" : "Josh", <2> + "lastname" : "Long", <2> +} +---- +<1> Read/wirte the keys `fn` & `ln` from/to the linkage document based on the lookup query. +<2> Use non _id_ fields for the lookup of the target documents. +==== + +.Document Reference reading target collection from linkage document +==== +[source,java] +---- +class Entity { + @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") <2> + private ReferencedObject ref; +} +---- + +[source,java] +---- +@WritingConverter +class ToDocumentPointerConverter implements Converter> { + public DocumentPointer convert(ReferencedObject source) { + return () -> new Document("id", source.id) <1> + .append("collection", ... ); <2> + } +} +---- + +[source,json] +---- +// entity +{ + "_id" : "8cfb002", + "ref" : { + "id" : "9a48e32", <1> + "collection" : "..." <2> + } +} +---- +<1> Read/wirte the keys `_id` from/to the linkage document to use them in the lookup query. +<2> The collection name can be read from the linkage document via its key. +==== + +[WARNING] +==== +We know it is tempting to use all kinds of MongoDB query operators in the lookup query and this is fine. But: + +* Make sure to have indexes in place that support your lookup. +* Mind that resolution takes time and consider a lazy strategy. +* A collection of document references is bulk loaded using an `$or` operator. + +The original element order is restored in memory which cannot be done when using MongoDB query operators. +In this case Results will be ordered as they are received from the store. + +And a few more general remarks: + +* Cyclic references? Ask your self if you need them. +* Lazy document references are hard to debug. Make sure tooling does not accidentally trigger proxy resolution by eg. calling `toString()`. +* There is no support for reading document references via the reactive bits Spring Data MongoDB offers. +==== + [[mapping-usage-events]] === Mapping Framework Events From e2a8c9553db58d4d76f2d26a081e0c61a3ccd97c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 18 May 2021 10:43:13 +0200 Subject: [PATCH 5/6] Polishing Rename ReferenceReader to ReferenceLookupDelegate. Rename LazyLoadingProxyGenerator to LazyLoadingProxyFactory. Rename DefaultReferenceLoader to MongoDatabaseFactoryReferenceLoader. Reduce scope of LookupFunction and move it to ReferenceLookupDelegate. Extract some checks into methods to reflect the underlying concepts. Simplify code, convert variables to constants where possible. --- .../core/convert/DefaultDbRefResolver.java | 9 ++- .../convert/DefaultReferenceResolver.java | 32 +++++--- .../core/convert/DocumentPointerFactory.java | 39 +++++++--- ...ator.java => LazyLoadingProxyFactory.java} | 58 +++++++------- .../core/convert/MappingMongoConverter.java | 38 ++++++---- ... MongoDatabaseFactoryReferenceLoader.java} | 8 +- .../core/convert/NoOpDbRefResolver.java | 10 +-- .../mongodb/core/convert/ReferenceLoader.java | 30 +++++--- ...ader.java => ReferenceLookupDelegate.java} | 76 +++++++++++-------- .../core/convert/ReferenceResolver.java | 33 ++------ .../mapping/BasicMongoPersistentProperty.java | 2 +- .../core/convert/LazyLoadingTestUtils.java | 7 +- .../core/convert/QueryMapperUnitTests.java | 9 ++- .../BasicMongoPersistentEntityUnitTests.java | 6 +- .../performance/ReactivePerformanceTests.java | 13 ++-- 15 files changed, 204 insertions(+), 166 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/{LazyLoadingProxyGenerator.java => LazyLoadingProxyFactory.java} (81%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/{DefaultReferenceLoader.java => MongoDatabaseFactoryReferenceLoader.java} (85%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/{ReferenceReader.java => ReferenceLookupDelegate.java} (84%) 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 5277fbc0b0..f64c7f0f06 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 @@ -47,6 +47,7 @@ import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; +import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisStd; @@ -83,7 +84,7 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db */ public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) { - super(new DefaultReferenceLoader(mongoDbFactory)); + super(new MongoDatabaseFactoryReferenceLoader(mongoDbFactory)); Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!"); @@ -117,7 +118,7 @@ public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr */ @Override public Document fetch(DBRef dbRef) { - return getReferenceLoader().fetch(DocumentReferenceQuery.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), + return getReferenceLoader().fetchOne(DocumentReferenceQuery.forSingleDocument(Filters.eq("_id", dbRef.getId())), ReferenceCollection.fromDBRef(dbRef)); } @@ -159,7 +160,7 @@ public List bulkFetch(List refs) { } List result = mongoCollection // - .find(new Document("_id", new Document("$in", ids))) // + .find(new Document(BasicMongoPersistentProperty.ID_FIELD_NAME, new Document("$in", ids))) // .into(new ArrayList<>()); return ids.stream() // @@ -239,7 +240,7 @@ private boolean isLazyDbRef(MongoPersistentProperty property) { private static Stream documentWithId(Object identifier, Collection documents) { return documents.stream() // - .filter(it -> it.get("_id").equals(identifier)) // + .filter(it -> it.get(BasicMongoPersistentProperty.ID_FIELD_NAME).equals(identifier)) // .limit(1); } 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 index 0692f719b5..7e38b6995d 100644 --- 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 @@ -15,7 +15,10 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.data.mongodb.core.mapping.DocumentReference; +import static org.springframework.data.mongodb.core.convert.ReferenceLookupDelegate.*; + +import java.util.Collections; + import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; @@ -37,21 +40,32 @@ public ReferenceLoader getReferenceLoader() { @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { + public Object resolveReference(MongoPersistentProperty property, Object source, + ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { + + LookupFunction lookupFunction = (filter, ctx) -> { + if (property.isCollectionLike() || property.isMap()) { + return getReferenceLoader().fetchMany(filter, ctx); + + } + + Object target = getReferenceLoader().fetchOne(filter, ctx); + return target == null ? Collections.emptyList() + : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx)); + }; if (isLazyReference(property)) { - return createLazyLoadingProxy(property, source, referenceReader, lookupFunction, resultConversionFunction); + return createLazyLoadingProxy(property, source, referenceLookupDelegate, lookupFunction, entityReader); } - return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); + return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader); } private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - ReferenceReader referenceReader, LookupFunction lookupFunction, - ResultConversionFunction resultConversionFunction) { - return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction, - resultConversionFunction); + ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, + MongoEntityReader entityReader) { + return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction, + entityReader); } protected boolean isLazyReference(MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java index a91a48d922..8e9554b6ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -17,12 +17,14 @@ import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bson.Document; + import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; @@ -37,9 +39,9 @@ */ class DocumentPointerFactory { - private ConversionService conversionService; - private MappingContext, MongoPersistentProperty> mappingContext; - private Map linkageMap; + private final ConversionService conversionService; + private final MappingContext, MongoPersistentProperty> mappingContext; + private final Map linkageMap; public DocumentPointerFactory(ConversionService conversionService, MappingContext, MongoPersistentProperty> mappingContext) { @@ -60,15 +62,24 @@ public DocumentPointer computePointer(MongoPersistentProperty property, Objec } else { MongoPersistentEntity persistentEntity = mappingContext - .getPersistentEntity(property.getAssociationTargetType()); + .getRequiredPersistentEntity(property.getAssociationTargetType()); - if (!property.getDocumentReference().lookup().toLowerCase().replaceAll("\\s", "").replaceAll("'", "") + // TODO: Extract method + if (!property.getDocumentReference().lookup().toLowerCase(Locale.ROOT).replaceAll("\\s", "").replaceAll("'", "") .equals("{_id:?#{#target}}")) { - return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), key -> { - return new LinkageDocument(key); - }).get(persistentEntity, - BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value)); + MongoPersistentEntity valueEntity = mappingContext.getPersistentEntity(value.getClass()); + PersistentPropertyAccessor propertyAccessor; + if (valueEntity == null) { + propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), + value); + } else { + propertyAccessor = valueEntity.getPropertyAccessor(value); + + } + + return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::new) + .get(persistentEntity, propertyAccessor); } // just take the id as a reference @@ -78,6 +89,8 @@ public DocumentPointer computePointer(MongoPersistentProperty property, Objec static class LinkageDocument { + static final Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); + String lookup; org.bson.Document fetchDocument; Map mapMap; @@ -87,16 +100,18 @@ public LinkageDocument(String lookup) { this.lookup = lookup; String targetLookup = lookup; - Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); Matcher matcher = pattern.matcher(lookup); int index = 0; mapMap = new LinkedHashMap<>(); + + // TODO: Make explicit what's happening here while (matcher.find()) { String expr = matcher.group(); - mapMap.put(Integer.valueOf(index), expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") - .replace("target.", "").replaceAll("'", "")); + String sanitized = expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") + .replace("target.", "").replaceAll("'", ""); + mapMap.put(index, sanitized); targetLookup = targetLookup.replace(expr, index + ""); index++; } 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/LazyLoadingProxyFactory.java similarity index 81% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyGenerator.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java index 570a516d9b..8c2156df2e 100644 --- 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/LazyLoadingProxyFactory.java @@ -15,47 +15,46 @@ */ package org.springframework.data.mongodb.core.convert; +import static org.springframework.data.mongodb.core.convert.ReferenceLookupDelegate.*; import static org.springframework.util.ReflectionUtils.*; import java.io.Serializable; import java.lang.reflect.Method; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; + 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.ReferenceResolver.LookupFunction; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.MongoEntityReader; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisStd; import org.springframework.util.ReflectionUtils; /** * @author Christoph Strobl */ -class LazyLoadingProxyGenerator { +class LazyLoadingProxyFactory { private final ObjenesisStd objenesis; - private final ReferenceReader referenceReader; + private final ReferenceLookupDelegate lookupDelegate; - public LazyLoadingProxyGenerator(ReferenceReader referenceReader) { + public LazyLoadingProxyFactory(ReferenceLookupDelegate lookupDelegate) { - this.referenceReader = referenceReader; + this.lookupDelegate = lookupDelegate; this.objenesis = new ObjenesisStd(true); } public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, - ResultConversionFunction resultConversionFunction) { + MongoEntityReader entityReader) { Class propertyType = property.getType(); - LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction, - resultConversionFunction); + LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, lookupDelegate, lookupFunction, + entityReader); if (!propertyType.isInterface()) { @@ -97,13 +96,13 @@ private Class getEnhancedTypeFor(Class type) { public static class LazyLoadingInterceptor implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable { - private final ReferenceReader referenceReader; - MongoPersistentProperty property; + private final ReferenceLookupDelegate referenceLookupDelegate; + private final MongoPersistentProperty property; private volatile boolean resolved; - private @org.springframework.lang.Nullable Object result; - private Object source; - private LookupFunction lookupFunction; - private ResultConversionFunction resultConversionFunction; + private @Nullable Object result; + private final Object source; + private final LookupFunction lookupFunction; + private final MongoEntityReader entityReader; private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD, GET_SOURCE_METHOD; @@ -118,22 +117,23 @@ public static class LazyLoadingInterceptor } } - public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader, - LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { + public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceLookupDelegate reader, + LookupFunction lookupFunction, MongoEntityReader entityReader) { this.property = property; this.source = source; - this.referenceReader = reader; + this.referenceLookupDelegate = reader; this.lookupFunction = lookupFunction; - this.resultConversionFunction = resultConversionFunction; + this.entityReader = entityReader; } @Nullable @Override - public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } + @Nullable @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable { @@ -180,6 +180,7 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox return method.invoke(target, args); } + @Nullable private Object ensureResolved() { if (!resolved) { @@ -190,7 +191,7 @@ private Object ensureResolved() { return this.result; } - private String proxyToString(Object source) { + private String proxyToString(@Nullable Object source) { StringBuilder description = new StringBuilder(); if (source != null) { @@ -203,7 +204,7 @@ private String proxyToString(Object source) { return description.toString(); } - private boolean proxyEquals(@org.springframework.lang.Nullable Object proxy, Object that) { + private boolean proxyEquals(@Nullable Object proxy, Object that) { if (!(that instanceof LazyLoadingProxy)) { return false; @@ -216,11 +217,11 @@ private boolean proxyEquals(@org.springframework.lang.Nullable Object proxy, Obj return proxyToString(proxy).equals(that.toString()); } - private int proxyHashCode(@org.springframework.lang.Nullable Object proxy) { + private int proxyHashCode(@Nullable Object proxy) { return proxyToString(proxy).hashCode(); } - @org.springframework.lang.Nullable + @Nullable private synchronized Object resolve() { if (resolved) { @@ -238,7 +239,7 @@ private synchronized Object resolve() { // property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()); // } - return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction); + return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader); } catch (RuntimeException ex) { throw ex; @@ -254,4 +255,5 @@ private synchronized Object resolve() { } } } + } 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 2ad4d75230..8a77b51e2f 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 @@ -38,6 +38,7 @@ import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -63,6 +64,7 @@ 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.BasicMongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -114,7 +116,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 final ReferenceLookupDelegate referenceLookupDelegate; protected @Nullable ApplicationContext applicationContext; protected MongoTypeMapper typeMapper; @@ -123,7 +125,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; - private DocumentPointerFactory documentPointerFactory; + private final DocumentPointerFactory documentPointerFactory; /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -154,7 +156,7 @@ public MappingMongoConverter(DbRefResolver dbRefResolver, return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator); }); - this.referenceReader = new ReferenceReader(mappingContext, () -> spELContext); + this.referenceLookupDelegate = new ReferenceLookupDelegate(mappingContext, spELContext); this.documentPointerFactory = new DocumentPointerFactory(conversionService, mappingContext); } @@ -361,16 +363,15 @@ private ParameterValueProvider getParameterProvider(Con parameterProvider); } - private S read(ConversionContext context, MongoPersistentEntity entity, Document bson) { + private S read(ConversionContext context, MongoPersistentEntity entity, Document bson) { SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); DocumentAccessor documentAccessor = new DocumentAccessor(bson); - if (bson.get("_id") != null) { - - Object existing = context.getPath().getPathItem(bson.get("_id"), entity.getCollection(), entity.getType()); + if (hasIdentifier(bson)) { + S existing = findContextualEntity(context, entity, bson); if (existing != null) { - return (S) existing; + return existing; } } @@ -391,6 +392,16 @@ private S read(ConversionContext context, MongoPersistentEnti return instance; } + private boolean hasIdentifier(Document bson) { + return bson.get(BasicMongoPersistentProperty.ID_FIELD_NAME) != null; + } + + @Nullable + private S findContextualEntity(ConversionContext context, MongoPersistentEntity entity, Document bson) { + return context.getPath().getPathItem(bson.get(BasicMongoPersistentProperty.ID_FIELD_NAME), entity.getCollection(), + entity.getType()); + } + private S populateProperties(ConversionContext context, MongoPersistentEntity entity, DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) { @@ -509,7 +520,7 @@ private void readAssociation(Association association, P ConversionContext context, SpELExpressionEvaluator evaluator) { MongoPersistentProperty property = association.getInverse(); - final Object value = documentAccessor.get(property); + Object value = documentAccessor.get(property); if (value == null) { return; @@ -521,18 +532,13 @@ private void readAssociation(Association association, P if (conversionService.canConvert(DocumentPointer.class, property.getActualType())) { - DocumentPointer pointer = new DocumentPointer() { - @Override - public Object getPointer() { - return value; - } - }; + DocumentPointer pointer = () -> value; // collection like special treatment accessor.setProperty(property, conversionService.convert(pointer, property.getActualType())); } else { accessor.setProperty(property, - dbRefResolver.resolveReference(property, value, referenceReader, context::convert)); + dbRefResolver.resolveReference(property, value, referenceLookupDelegate, context::convert)); } return; } 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/MongoDatabaseFactoryReferenceLoader.java similarity index 85% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceLoader.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoDatabaseFactoryReferenceLoader.java index 66b698077b..2483f57543 100644 --- 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/MongoDatabaseFactoryReferenceLoader.java @@ -29,13 +29,13 @@ /** * @author Christoph Strobl */ -public class DefaultReferenceLoader implements ReferenceLoader { +public class MongoDatabaseFactoryReferenceLoader implements ReferenceLoader { - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultReferenceLoader.class); + private static final Logger LOGGER = LoggerFactory.getLogger(MongoDatabaseFactoryReferenceLoader.class); private final MongoDatabaseFactory mongoDbFactory; - public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) { + public MongoDatabaseFactoryReferenceLoader(MongoDatabaseFactory mongoDbFactory) { Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!"); @@ -43,7 +43,7 @@ public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) { } @Override - public Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context) { + public Iterable fetchMany(DocumentReferenceQuery filter, ReferenceCollection context) { MongoCollection collection = getCollection(context); 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 8b6c969439..41d7ab3c12 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,13 +16,10 @@ 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.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; + import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -76,9 +73,8 @@ private T handle() throws UnsupportedOperationException { @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - LookupFunction lookupFunction, - ResultConversionFunction resultConversionFunction) { + public Object resolveReference(MongoPersistentProperty property, Object source, + ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { return null; } 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 index d5c72afad8..7cfd5e3153 100644 --- 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 @@ -31,13 +31,13 @@ public interface ReferenceLoader { @Nullable - default Document fetch(DocumentReferenceQuery filter, ReferenceCollection context) { + default Document fetchOne(DocumentReferenceQuery filter, ReferenceCollection context) { - Iterator it = bulkFetch(filter, context).iterator(); + Iterator it = fetchMany(filter, context).iterator(); return it.hasNext() ? it.next() : null; } - Iterable bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context); + Iterable fetchMany(DocumentReferenceQuery filter, ReferenceCollection context); interface DocumentReferenceQuery { @@ -52,16 +52,12 @@ default Bson getSort() { default Iterable apply(MongoCollection collection) { return restoreOrder(collection.find(getFilter()).sort(getSort())); } - + default Iterable restoreOrder(Iterable documents) { return documents; } - static DocumentReferenceQuery referenceFilter(Bson bson) { - return () -> bson; - } - - static DocumentReferenceQuery singleReferenceFilter(Bson bson) { + static DocumentReferenceQuery forSingleDocument(Bson bson) { return new DocumentReferenceQuery() { @@ -78,6 +74,22 @@ public Iterable apply(MongoCollection collection) { } }; } + + static DocumentReferenceQuery forManyDocuments(Bson bson) { + + return new DocumentReferenceQuery() { + + @Override + public Bson getFilter() { + return bson; + } + + @Override + public Iterable apply(MongoCollection collection) { + return collection.find(getFilter()).sort(getSort()); + } + }; + } } } 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/ReferenceLookupDelegate.java similarity index 84% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceReader.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index fb37367b1d..3c441c1388 100644 --- 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/ReferenceLookupDelegate.java @@ -29,12 +29,12 @@ 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.DocumentReferenceQuery; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.LookupFunction; +import org.springframework.data.mongodb.core.convert.ReferenceResolver.MongoEntityReader; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; -import org.springframework.data.mongodb.core.convert.ReferenceResolver.ResultConversionFunction; import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -42,58 +42,59 @@ 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.Assert; import org.springframework.util.StringUtils; import com.mongodb.DBRef; import com.mongodb.client.MongoCollection; /** + * A common delegate for {@link ReferenceResolver} implementations to resolve a reference to one/many target documents + * that are converted to entities. + * * @author Christoph Strobl + * @author Mark Paluch */ -public class ReferenceReader { +public final class ReferenceLookupDelegate { - private final Lazy, MongoPersistentProperty>> mappingContext; - private final Supplier spelContextSupplier; + private final MappingContext, MongoPersistentProperty> mappingContext; + private final SpELContext spELContext; private final ParameterBindingDocumentCodec codec; - public ReferenceReader(MappingContext, MongoPersistentProperty> mappingContext, - Supplier spelContextSupplier) { - - this(() -> mappingContext, spelContextSupplier); - } + public ReferenceLookupDelegate( + MappingContext, MongoPersistentProperty> mappingContext, + SpELContext spELContext) { - public ReferenceReader( - Supplier, MongoPersistentProperty>> mappingContextSupplier, - Supplier spelContextSupplier) { + Assert.notNull(mappingContext, "MappingContext must not be null"); + Assert.notNull(spELContext, "SpELContext must not be null"); - this.mappingContext = Lazy.of(mappingContextSupplier); - this.spelContextSupplier = spelContextSupplier; + this.mappingContext = mappingContext; + this.spELContext = spELContext; this.codec = new ParameterBindingDocumentCodec(); } + @Nullable Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, - ResultConversionFunction resultConversionFunction) { + MongoEntityReader entityReader) { - SpELContext spELContext = spelContextSupplier.get(); DocumentReferenceQuery filter = computeFilter(property, value, spELContext); ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext); Iterable result = lookupFunction.apply(filter, referenceCollection); - if (!result.iterator().hasNext()) { - return null; + if (property.isCollectionLike()) { + return entityReader.read(result, property.getTypeInformation()); } - if (property.isCollectionLike()) { - return resultConversionFunction.apply(result, property.getTypeInformation()); + if (!result.iterator().hasNext()) { + return null; } - return resultConversionFunction.apply(result.iterator().next(), property.getTypeInformation()); + return entityReader.read(result.iterator().next(), property.getTypeInformation()); } private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, @@ -107,6 +108,8 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop return ReferenceCollection.fromDBRef((DBRef) value); } + String collection = mappingContext.getRequiredPersistentEntity(property.getAssociationTargetType()).getCollection(); + if (value instanceof Document) { Document ref = (Document) value; @@ -120,12 +123,12 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop () -> ref.get("db", String.class)); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> ref.get("collection", - mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); + collection)); return new ReferenceCollection(targetDatabase, targetCollection); } return new ReferenceCollection(ref.getString("db"), ref.get("collection", - mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection())); + collection)); } if (property.isDocumentReference()) { @@ -135,16 +138,16 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, - () -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); + () -> collection); return new ReferenceCollection(targetDatabase, targetCollection); } return new ReferenceCollection(null, - mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()); + collection); } - @Nullable + @SuppressWarnings("unchecked") private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier defaultValue) { if (!StringUtils.hasText(value)) { @@ -153,7 +156,7 @@ private T parseValueOrGet(String value, ParameterBindingContext bindingConte if (!BsonUtils.isJsonDocument(value) && value.contains("?#{")) { String s = "{ 'target-value' : " + value + "}"; - T evaluated = (T) codec.decode(s, bindingContext).get("target-value "); + T evaluated = (T) codec.decode(s, bindingContext).get("target-value"); return evaluated != null ? evaluated : defaultValue.get(); } @@ -186,6 +189,7 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object return ctx; } + @SuppressWarnings("unchecked") DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { DocumentReference documentReference = property.getDocumentReference(); @@ -196,7 +200,7 @@ DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object va if (property.isCollectionLike() && value instanceof Collection) { List ors = new ArrayList<>(); - for (Object entry : (Collection) value) { + for (Object entry : (Collection) value) { Document decoded = codec.decode(lookup, bindingContext(property, entry, spELContext)); ors.add(decoded); @@ -209,7 +213,7 @@ DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object va Map filterMap = new LinkedHashMap<>(); - for (Entry entry : ((Map) value).entrySet()) { + for (Entry entry : ((Map) value).entrySet()) { Document decoded = codec.decode(lookup, bindingContext(property, entry.getValue(), spELContext)); filterMap.put(entry.getKey(), decoded); @@ -321,9 +325,9 @@ public Document getSort() { int compareAgainstReferenceIndex(List referenceList, Document document1, Document document2) { - for (int i = 0; i < referenceList.size(); i++) { + for (Document document : referenceList) { - Set> entries = referenceList.get(i).entrySet(); + Set> entries = document.entrySet(); if (document1.entrySet().containsAll(entries)) { return -1; } @@ -334,4 +338,10 @@ int compareAgainstReferenceIndex(List referenceList, Document document return referenceList.size(); } } + + @FunctionalInterface + interface LookupFunction { + + Iterable apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection); + } } 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 index f29dc16a7c..dae2043b4b 100644 --- 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 @@ -15,13 +15,10 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.Collections; - -import org.bson.Document; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import com.mongodb.DBRef; @@ -31,22 +28,8 @@ public interface ReferenceResolver { @Nullable - Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction); - - default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, - ResultConversionFunction resultConversionFunction) { - - return resolveReference(property, source, referenceReader, (filter, ctx) -> { - if (property.isCollectionLike() || property.isMap()) { - return getReferenceLoader().bulkFetch(filter, ctx); - - } - - Object target = getReferenceLoader().fetch(filter, ctx); - return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetch(filter, ctx)); - }, resultConversionFunction); - } + Object resolveReference(MongoPersistentProperty property, Object source, + ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader); ReferenceLoader getReferenceLoader(); @@ -58,6 +41,8 @@ class ReferenceCollection { public ReferenceCollection(@Nullable String database, String collection) { + Assert.hasText(collection, "Collection must not be empty or null"); + this.database = database; this.collection = collection; } @@ -76,13 +61,9 @@ public String getDatabase() { } } - @FunctionalInterface - interface LookupFunction { - Iterable apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection); - } @FunctionalInterface - interface ResultConversionFunction { - Object apply(Object source, TypeInformation property); + interface MongoEntityReader { + Object read(Object source, TypeInformation property); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index b7b71a7fee..53af00fc54 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -47,7 +47,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope private static final Logger LOG = LoggerFactory.getLogger(BasicMongoPersistentProperty.class); - private static final String ID_FIELD_NAME = "_id"; + public static final String ID_FIELD_NAME = "_id"; private static final String LANGUAGE_FIELD_NAME = "language"; private static final Set> SUPPORTED_ID_TYPES = new HashSet>(); private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); 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 f5d43c8ef0..91afb8c6ec 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 @@ -54,7 +54,8 @@ public static void assertProxyIsResolved(Object target, boolean expected) { public static void assertProxy(Object proxy, Consumer verification) { - LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor = (LazyLoadingProxyGenerator.LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice() + LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor = (LazyLoadingProxyFactory.LazyLoadingInterceptor) (proxy instanceof Advised + ? ((Advised) proxy).getAdvisors()[0].getAdvice() : ((Factory) proxy).getCallback(0)); verification.accept(new LazyLoadingProxyValueRetriever(interceptor)); @@ -67,9 +68,9 @@ private static LazyLoadingInterceptor extractInterceptor(Object proxy) { public static class LazyLoadingProxyValueRetriever { - LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor; + LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor; - public LazyLoadingProxyValueRetriever(LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor) { + public LazyLoadingProxyValueRetriever(LazyLoadingProxyFactory.LazyLoadingInterceptor interceptor) { this.interceptor = interceptor; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 9c157db759..d371b32c12 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -1501,18 +1501,19 @@ static class WithDocumentReferences { SimpeEntityWithoutId noIdButLookupQuery; } - + + // TODO @Test void xxx() { - + Sample sample = new Sample(); sample.foo = "sample-id"; Query query = query(where("sample").is(sample)); - + org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDocumentReferences.class)); - + System.out.println("mappedObject.toJson(): " + mappedObject.toJson()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index 28d5123502..9c898d28ce 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -30,11 +30,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AliasFor; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.MappingException; -import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; @@ -351,6 +350,9 @@ private static class DocumentWithComposedAnnotation {} @Document("#{myProperty}") class MappedWithExtension {} + @Document("${value.from.file}") + class MappedWithValue {} + @Document(collation = "#{myCollation}") class WithCollationFromSpEL {} 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 b70930dae0..8a462a9370 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,20 +18,13 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; -import org.springframework.data.mongodb.core.convert.ReferenceLoader; -import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; -import org.springframework.data.mongodb.core.convert.ReferenceReader; -import org.springframework.data.util.TypeInformation; -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; @@ -48,12 +41,15 @@ import org.springframework.data.mongodb.core.convert.DbRefResolverCallback; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.ReferenceLoader; +import org.springframework.data.mongodb.core.convert.ReferenceLookupDelegate; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; @@ -106,7 +102,8 @@ public void setUp() throws Exception { @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) { + public Object resolveReference(MongoPersistentProperty property, Object source, + ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { return null; } From eb08e79c8abeefae02575ffec8dda7e66d609d3d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 19 May 2021 11:30:05 +0200 Subject: [PATCH 6/6] Avoid capturing lambdas, update javadoc and add tests. Also allow direct usage of (at)Reference from data commons to define associations. --- .../convert/DefaultReferenceResolver.java | 69 +++--- .../core/convert/DocumentPointerFactory.java | 201 +++++++++++++----- .../core/convert/MappingMongoConverter.java | 45 ++-- .../MongoDatabaseFactoryReferenceLoader.java | 19 +- .../mongodb/core/convert/MongoWriter.java | 16 +- .../core/convert/NoOpDbRefResolver.java | 5 - .../mongodb/core/convert/QueryMapper.java | 5 +- .../mongodb/core/convert/ReferenceLoader.java | 62 ++++-- .../core/convert/ReferenceLookupDelegate.java | 182 ++++++++++++---- .../core/convert/ReferenceResolver.java | 60 +++++- .../data/mongodb/util/BsonUtils.java | 8 +- .../MongoTemplateDocumentReferenceTests.java | 105 ++++++++- .../DocumentPointerFactoryUnitTests.java | 139 ++++++++++++ .../MappingMongoConverterUnitTests.java | 10 +- .../performance/ReactivePerformanceTests.java | 4 - src/main/asciidoc/reference/mapping.adoc | 6 +- 16 files changed, 747 insertions(+), 189 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java 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 index 7e38b6995d..a678fd7da6 100644 --- 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 @@ -19,40 +19,45 @@ import java.util.Collections; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** + * {@link ReferenceResolver} implementation that uses a given {@link ReferenceLookupDelegate} to load and convert entity + * associations expressed via a {@link MongoPersistentProperty persitent property}. Creates {@link LazyLoadingProxy + * proxies} for associations that should be lazily loaded. + * * @author Christoph Strobl */ public class DefaultReferenceResolver implements ReferenceResolver { private final ReferenceLoader referenceLoader; + private final LookupFunction collectionLookupFunction = (filter, ctx) -> getReferenceLoader().fetchMany(filter, ctx); + private final LookupFunction singleValueLookupFunction = (filter, ctx) -> { + Object target = getReferenceLoader().fetchOne(filter, ctx); + return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx)); + }; + + /** + * Create a new instance of {@link DefaultReferenceResolver}. + * + * @param referenceLoader must not be {@literal null}. + */ public DefaultReferenceResolver(ReferenceLoader referenceLoader) { + + Assert.notNull(referenceLoader, "ReferenceLoader must not be null!"); this.referenceLoader = referenceLoader; } - @Override - public ReferenceLoader getReferenceLoader() { - return referenceLoader; - } - - @Nullable @Override public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { - LookupFunction lookupFunction = (filter, ctx) -> { - if (property.isCollectionLike() || property.isMap()) { - return getReferenceLoader().fetchMany(filter, ctx); - - } - - Object target = getReferenceLoader().fetchOne(filter, ctx); - return target == null ? Collections.emptyList() - : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx)); - }; + LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction + : singleValueLookupFunction; if (isLazyReference(property)) { return createLazyLoadingProxy(property, source, referenceLookupDelegate, lookupFunction, entityReader); @@ -61,13 +66,14 @@ public Object resolveReference(MongoPersistentProperty property, Object source, return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader); } - private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, - ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, - MongoEntityReader entityReader) { - return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction, - entityReader); - } - + /** + * Check if the association expressed by the given {@link MongoPersistentProperty property} should be resolved lazily. + * + * @param property + * @return return {@literal true} if the defined association is lazy. + * @see DBRef#lazy() + * @see DocumentReference#lazy() + */ protected boolean isLazyReference(MongoPersistentProperty property) { if (property.isDocumentReference()) { @@ -76,4 +82,19 @@ protected boolean isLazyReference(MongoPersistentProperty property) { return property.getDBRef() != null && property.getDBRef().lazy(); } + + /** + * The {@link ReferenceLoader} executing the lookup. + * + * @return never {@literal null}. + */ + protected ReferenceLoader getReferenceLoader() { + return referenceLoader; + } + + private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, + ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) { + return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction, + entityReader); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java index 8e9554b6ba..09d69e4b27 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -15,18 +15,20 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.WeakHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bson.Document; - import org.springframework.core.convert.ConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.annotation.Reference; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; import org.springframework.data.mongodb.core.mapping.DocumentPointer; @@ -34,6 +36,10 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; /** + * Internal API to construct {@link DocumentPointer} for a given property. Considers {@link LazyLoadingProxy}, + * registered {@link Object} to {@link DocumentPointer} {@link org.springframework.core.convert.converter.Converter}, + * simple {@literal _id} lookups and cases where the {@link DocumentPointer} needs to be computed via a lookup query. + * * @author Christoph Strobl * @since 3.3 */ @@ -41,17 +47,29 @@ class DocumentPointerFactory { private final ConversionService conversionService; private final MappingContext, MongoPersistentProperty> mappingContext; - private final Map linkageMap; - - public DocumentPointerFactory(ConversionService conversionService, + private final Map cache; + + /** + * A {@link Pattern} matching quoted and unquoted variants (with/out whitespaces) of + * {'_id' : ?#{#target} }. + */ + private static final Pattern DEFAULT_LOOKUP_PATTERN = Pattern.compile("\\{\\s?" + // document start (whitespace opt) + "['\"]?_id['\"]?" + // followed by an optionally quoted _id. Like: _id, '_id' or "_id" + "?\\s?:\\s?" + // then a colon optionally wrapped inside whitespaces + "['\"]?\\?#\\{#target\\}['\"]?" + // leading to the potentially quoted ?#{#target} expression + "\\s*}"); // some optional whitespaces and document close + + DocumentPointerFactory(ConversionService conversionService, MappingContext, MongoPersistentProperty> mappingContext) { this.conversionService = conversionService; this.mappingContext = mappingContext; - this.linkageMap = new HashMap<>(); + this.cache = new WeakHashMap<>(); } - public DocumentPointer computePointer(MongoPersistentProperty property, Object value, Class typeHint) { + DocumentPointer computePointer( + MappingContext, MongoPersistentProperty> mappingContext, + MongoPersistentProperty property, Object value, Class typeHint) { if (value instanceof LazyLoadingProxy) { return () -> ((LazyLoadingProxy) value).getSource(); @@ -59,92 +77,161 @@ public DocumentPointer computePointer(MongoPersistentProperty property, Objec if (conversionService.canConvert(typeHint, DocumentPointer.class)) { return conversionService.convert(value, DocumentPointer.class); - } else { + } - MongoPersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(property.getAssociationTargetType()); + MongoPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(property.getAssociationTargetType()); - // TODO: Extract method - if (!property.getDocumentReference().lookup().toLowerCase(Locale.ROOT).replaceAll("\\s", "").replaceAll("'", "") - .equals("{_id:?#{#target}}")) { + if (usesDefaultLookup(property)) { + return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); + } - MongoPersistentEntity valueEntity = mappingContext.getPersistentEntity(value.getClass()); - PersistentPropertyAccessor propertyAccessor; - if (valueEntity == null) { - propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), - value); - } else { - propertyAccessor = valueEntity.getPropertyAccessor(value); + MongoPersistentEntity valueEntity = mappingContext.getPersistentEntity(value.getClass()); + PersistentPropertyAccessor propertyAccessor; + if (valueEntity == null) { + propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value); + } else { + propertyAccessor = valueEntity.getPropertyPathAccessor(value); + } - } + return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from) + .getDocumentPointer(mappingContext, persistentEntity, propertyAccessor); + } - return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::new) - .get(persistentEntity, propertyAccessor); - } + private boolean usesDefaultLookup(MongoPersistentProperty property) { - // just take the id as a reference - return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); + if (property.isDocumentReference()) { + return DEFAULT_LOOKUP_PATTERN.matcher(property.getDocumentReference().lookup()).matches(); + } + + Reference atReference = property.findAnnotation(Reference.class); + if (atReference != null) { + return true; } + + throw new IllegalStateException(String.format("%s does not seem to be define Reference", property)); } + /** + * Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and + * inverting it. + * + *
+	 * // source
+	 * { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} }
+	 * 
+	 * // target
+	 * { 'fn' : ..., 'ln' : ... }
+	 * 
+ * + * The actual pointer is the computed via + * {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from + * the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions + * from the source. + */ static class LinkageDocument { - static final Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); + static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\?#\\{#?(?[\\w\\d\\.\\-)]*)\\}"); + static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("###_(?\\d*)_###"); - String lookup; - org.bson.Document fetchDocument; - Map mapMap; + private final String lookup; + private final org.bson.Document documentPointer; + private final Map placeholderMap; - public LinkageDocument(String lookup) { + static LinkageDocument from(String lookup) { + return new LinkageDocument(lookup); + } - this.lookup = lookup; - String targetLookup = lookup; + private LinkageDocument(String lookup) { + this.lookup = lookup; + this.placeholderMap = new LinkedHashMap<>(); - Matcher matcher = pattern.matcher(lookup); int index = 0; - mapMap = new LinkedHashMap<>(); + Matcher matcher = EXPRESSION_PATTERN.matcher(lookup); + String targetLookup = lookup; - // TODO: Make explicit what's happening here while (matcher.find()) { - String expr = matcher.group(); - String sanitized = expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") - .replace("target.", "").replaceAll("'", ""); - mapMap.put(index, sanitized); - targetLookup = targetLookup.replace(expr, index + ""); + String expression = matcher.group(); + String fieldName = matcher.group("fieldName").replace("target.", ""); + + String placeholder = placeholder(index); + placeholderMap.put(placeholder, fieldName); + targetLookup = targetLookup.replace(expression, "'" + placeholder + "'"); index++; } - fetchDocument = org.bson.Document.parse(targetLookup); + this.documentPointer = org.bson.Document.parse(targetLookup); } - org.bson.Document get(MongoPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor) { + private String placeholder(int index) { + return "###_" + index + "_###"; + } - org.bson.Document targetDocument = new Document(); + private boolean isPlaceholder(String key) { + return PLACEHOLDER_PATTERN.matcher(key).matches(); + } - // TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing? - // like we have it ordered by index values and could provide the parameter array from it. + DocumentPointer getDocumentPointer( + MappingContext, MongoPersistentProperty> mappingContext, + MongoPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor) { + return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity, + propertyAccessor); + } + + Document updatePlaceholders(org.bson.Document source, org.bson.Document target, + MappingContext, MongoPersistentProperty> mappingContext, + MongoPersistentEntity persistentEntity, PersistentPropertyAccessor propertyAccessor) { - for (Entry entry : fetchDocument.entrySet()) { + for (Entry entry : source.entrySet()) { + + if (entry.getKey().startsWith("$")) { + throw new InvalidDataAccessApiUsageException(String.format( + "Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.", + lookup, entry.getKey())); + } - if (entry.getKey().equals("target")) { + if (entry.getValue() instanceof Document) { - String refKey = mapMap.get(entry.getValue()); + MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey()); + if (persistentProperty != null && persistentProperty.isEntity()) { - if (persistentEntity.hasIdProperty()) { - targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty())); + MongoPersistentEntity nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType()); + target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, + nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty)))); } else { - targetDocument.put(refKey, propertyAccessor.getBean()); + target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, + persistentEntity, propertyAccessor)); } continue; } - Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey())); - String refKey = mapMap.get(entry.getValue()); - targetDocument.put(refKey, target); + if (placeholderMap.containsKey(entry.getValue())) { + + String attribute = placeholderMap.get(entry.getValue()); + if (attribute.contains(".")) { + attribute = attribute.substring(attribute.lastIndexOf('.') + 1); + } + + String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey(); + if (!fieldName.contains(".")) { + + Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName)); + target.put(attribute, targetValue); + continue; + } + + PersistentPropertyPath path = mappingContext + .getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation())); + Object targetValue = propertyAccessor.getProperty(path); + target.put(attribute, targetValue); + continue; + } + + target.put(entry.getKey(), entry.getValue()); } - return targetDocument; + return target; } } } 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 8a77b51e2f..87f0adeb62 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 @@ -45,6 +45,7 @@ import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.annotation.Reference; import org.springframework.data.convert.TypeMapper; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; @@ -526,7 +527,7 @@ private void readAssociation(Association association, P return; } - if (property.isDocumentReference()) { + if (property.isDocumentReference() || (!property.isDbReference() && property.findAnnotation(Reference.class) != null)) { // quite unusual but sounds like worth having? @@ -587,43 +588,46 @@ public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringP return createDBRef(object, referringProperty); } - public Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { + @Override + public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (source instanceof LazyLoadingProxy) { - return ((LazyLoadingProxy) source).getSource(); + return () -> ((LazyLoadingProxy) source).getSource(); } - if (referringProperty != null) { + Assert.notNull(referringProperty, "Cannot create DocumentReference. The referringProperty must not be null!"); if (referringProperty.isDbReference()) { - return toDBRef(source, referringProperty); + return () -> toDBRef(source, referringProperty); } - if (referringProperty.isDocumentReference()) { + + if (referringProperty.isDocumentReference() || referringProperty.findAnnotation(Reference.class) != null) { return createDocumentPointer(source, referringProperty); } - } - throw new RuntimeException("oops - what's that " + source); + throw new IllegalArgumentException("The referringProperty is neither a DBRef nor a document reference"); } - Object createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { + DocumentPointer createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (referringProperty == null) { - return source; + return () -> source; + } + + if(source instanceof DocumentPointer) { + return (DocumentPointer) source; } if (ClassUtils.isAssignableValue(referringProperty.getType(), source) && conversionService.canConvert(referringProperty.getType(), DocumentPointer.class)) { - return conversionService.convert(source, DocumentPointer.class).getPointer(); + return conversionService.convert(source, DocumentPointer.class); } if (ClassUtils.isAssignableValue(referringProperty.getAssociationTargetType(), source)) { - return documentPointerFactory.computePointer(referringProperty, source, referringProperty.getActualType()) - .getPointer(); - + return documentPointerFactory.computePointer(mappingContext, referringProperty, source, referringProperty.getActualType()); } - return source; + return () -> source; } /** @@ -813,7 +817,7 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce if (prop.isAssociation()) { accessor.put(prop, new DocumentPointerFactory(conversionService, mappingContext) - .computePointer(prop, obj, valueType.getType()).getPointer()); + .computePointer(mappingContext, prop, obj, valueType.getType()).getPointer()); return; } @@ -864,13 +868,14 @@ protected List createCollection(Collection collection, MongoPersisten return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it) .getIdentifier(); } - }).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new BasicDBList()); + }).collect(Collectors.toList()), ClassTypeInformation.from(DocumentPointer.class), new ArrayList<>()); } if (property.hasExplicitWriteTarget()) { return writeCollectionInternal(collection, new FieldTypeInformation<>(property), new ArrayList<>()); } - return writeCollectionInternal(collection, property.getTypeInformation(), new BasicDBList()); + + return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>()); } List dbList = new ArrayList<>(collection.size()); @@ -960,7 +965,7 @@ private List writeCollectionInternal(Collection source, @Nullable Typ collection.add(getPotentiallyConvertedSimpleWrite(element, componentType != null ? componentType.getType() : Object.class)); } else if (element instanceof Collection || elementType.isArray()) { - collection.add(writeCollectionInternal(BsonUtils.asCollection(element), componentType, new BasicDBList())); + collection.add(writeCollectionInternal(BsonUtils.asCollection(element), componentType, new ArrayList<>())); } else { Document document = new Document(); writeInternal(element, document, componentType); @@ -992,7 +997,7 @@ protected Bson writeMapInternal(Map obj, Bson bson, TypeInformat writeSimpleInternal(val, bson, simpleKey); } else if (val instanceof Collection || val.getClass().isArray()) { BsonUtils.addToMap(bson, simpleKey, - writeCollectionInternal(BsonUtils.asCollection(val), propertyType.getMapValueType(), new BasicDBList())); + writeCollectionInternal(BsonUtils.asCollection(val), propertyType.getMapValueType(), new ArrayList<>())); } else { Document document = new Document(); TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoDatabaseFactoryReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoDatabaseFactoryReferenceLoader.java index 2483f57543..0973e5a5fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoDatabaseFactoryReferenceLoader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoDatabaseFactoryReferenceLoader.java @@ -27,6 +27,9 @@ import com.mongodb.client.MongoCollection; /** + * {@link ReferenceLoader} implementation using a {@link MongoDatabaseFactory} to obtain raw {@link Document documents} + * for linked entities via a {@link ReferenceLoader.DocumentReferenceQuery}. + * * @author Christoph Strobl */ public class MongoDatabaseFactoryReferenceLoader implements ReferenceLoader { @@ -35,6 +38,9 @@ public class MongoDatabaseFactoryReferenceLoader implements ReferenceLoader { private final MongoDatabaseFactory mongoDbFactory; + /** + * @param mongoDbFactory must not be {@literal null}. + */ public MongoDatabaseFactoryReferenceLoader(MongoDatabaseFactory mongoDbFactory) { Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!"); @@ -43,20 +49,27 @@ public MongoDatabaseFactoryReferenceLoader(MongoDatabaseFactory mongoDbFactory) } @Override - public Iterable fetchMany(DocumentReferenceQuery filter, ReferenceCollection context) { + public Iterable fetchMany(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { MongoCollection collection = getCollection(context); if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Bulk fetching {} from {}.{}.", filter, + LOGGER.trace("Bulk fetching {} from {}.{}.", referenceQuery, StringUtils.hasText(context.getDatabase()) ? context.getDatabase() : collection.getNamespace().getDatabaseName(), context.getCollection()); } - return filter.apply(collection); + return referenceQuery.apply(collection); } + /** + * Obtain the {@link MongoCollection} for a given {@link ReferenceCollection} from the underlying + * {@link MongoDatabaseFactory}. + * + * @param context must not be {@literal null}. + * @return the {@link MongoCollection} targeted by the {@link ReferenceCollection}. + */ protected MongoCollection getCollection(ReferenceCollection context) { return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(), diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index 779b3236d3..6bef57cb63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -17,6 +17,8 @@ import org.bson.conversions.Bson; import org.springframework.data.convert.EntityWriter; +import org.springframework.data.mongodb.core.mapping.DocumentPointer; +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.util.TypeInformation; @@ -61,6 +63,7 @@ default Object convertToMongoType(@Nullable Object obj) { default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { return convertToMongoType(obj, entity.getTypeInformation()); } + /** * Creates a {@link DBRef} to refer to the given object. * @@ -71,7 +74,16 @@ default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity */ DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referingProperty); - default Object toDocumentReference(Object source, @Nullable MongoPersistentProperty referringProperty) { - return toDBRef(source, referringProperty); + /** + * Creates a the {@link DocumentPointer} representing the link to another entity. + * + * @param source the object to create a document link to. + * @param referringProperty the client-side property referring to the object which might carry additional metadata for + * the {@link DBRef} object to create. Can be {@literal null}. + * @return will never be {@literal null}. + * @since 3.3 + */ + default DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { + return () -> toDBRef(source, referringProperty); } } 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 41d7ab3c12..587d1a5047 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 @@ -77,9 +77,4 @@ public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { return null; } - - @Override - public ReferenceLoader getReferenceLoader() { - return handle(); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 36353e4f86..81c1c96ddf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -26,6 +26,7 @@ import org.bson.types.ObjectId; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Reference; import org.springframework.data.domain.Example; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; @@ -672,8 +673,8 @@ private Object createReferenceFor(Object source, MongoPersistentProperty propert return (DBRef) source; } - if(property != null && property.isDocumentReference()) { - return converter.toDocumentReference(source, property); + if(property != null && (property.isDocumentReference() || (!property.isDbReference() && property.findAnnotation(Reference.class) != null))) { + return converter.toDocumentPointer(source, property).getPointer(); } return converter.toDBRef(source, property); 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 index 7cfd5e3153..2f96f57da2 100644 --- 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 @@ -26,33 +26,70 @@ import com.mongodb.client.MongoCollection; /** + * The {@link ReferenceLoader} obtains raw {@link Document documents} for linked entities via a + * {@link ReferenceLoader.DocumentReferenceQuery}. + * * @author Christoph Strobl + * @since 3.3 */ public interface ReferenceLoader { + /** + * Obtain a single {@link Document} matching the given {@literal referenceQuery} in the {@literal context}. + * + * @param referenceQuery must not be {@literal null}. + * @param context must not be {@literal null}. + * @return the matching {@link Document} or {@literal null} if none found. + */ @Nullable - default Document fetchOne(DocumentReferenceQuery filter, ReferenceCollection context) { + default Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { - Iterator it = fetchMany(filter, context).iterator(); + Iterator it = fetchMany(referenceQuery, context).iterator(); return it.hasNext() ? it.next() : null; } - Iterable fetchMany(DocumentReferenceQuery filter, ReferenceCollection context); - + /** + * Obtain multiple {@link Document} matching the given {@literal referenceQuery} in the {@literal context}. + * + * @param referenceQuery must not be {@literal null}. + * @param context must not be {@literal null}. + * @return the matching {@link Document} or {@literal null} if none found. + */ + Iterable fetchMany(DocumentReferenceQuery referenceQuery, ReferenceCollection context); + + /** + * The {@link DocumentReferenceQuery} defines the criteria by which {@link Document documents} should be matched + * applying potentially given order criteria. + */ interface DocumentReferenceQuery { - Bson getFilter(); - + /** + * Get the query to obtain matching {@link Document documents}. + * + * @return never {@literal null}. + */ + Bson getQuery(); + + /** + * Get the sort criteria for ordering results. + * + * @return an empty {@link Document} by default. Never {@literal null}. + */ default Bson getSort() { return new Document(); } // TODO: Move apply method into something else that holds the collection and knows about single item/multi-item - // processing default Iterable apply(MongoCollection collection) { - return restoreOrder(collection.find(getFilter()).sort(getSort())); + return restoreOrder(collection.find(getQuery()).sort(getSort())); } + /** + * Restore the order of fetched documents. + * + * @param documents must not be {@literal null}. + * @return never {@literal null}. + */ default Iterable restoreOrder(Iterable documents) { return documents; } @@ -62,14 +99,14 @@ static DocumentReferenceQuery forSingleDocument(Bson bson) { return new DocumentReferenceQuery() { @Override - public Bson getFilter() { + public Bson getQuery() { return bson; } @Override public Iterable apply(MongoCollection collection) { - Document result = collection.find(getFilter()).sort(getSort()).limit(1).first(); + Document result = collection.find(getQuery()).sort(getSort()).limit(1).first(); return result != null ? Collections.singleton(result) : Collections.emptyList(); } }; @@ -80,16 +117,15 @@ static DocumentReferenceQuery forManyDocuments(Bson bson) { return new DocumentReferenceQuery() { @Override - public Bson getFilter() { + public Bson getQuery() { return bson; } @Override public Iterable apply(MongoCollection collection) { - return collection.find(getFilter()).sort(getSort()); + return collection.find(getQuery()).sort(getSort()); } }; } } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index 3c441c1388..09f4c1a8ae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,7 +30,6 @@ 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.DocumentReferenceQuery; @@ -64,6 +64,12 @@ public final class ReferenceLookupDelegate { private final SpELContext spELContext; private final ParameterBindingDocumentCodec codec; + /** + * Create a new {@link ReferenceLookupDelegate}. + * + * @param mappingContext must not be {@literal null}. + * @param spELContext must not be {@literal null}. + */ public ReferenceLookupDelegate( MappingContext, MongoPersistentProperty> mappingContext, SpELContext spELContext) { @@ -76,11 +82,20 @@ public ReferenceLookupDelegate( this.codec = new ParameterBindingDocumentCodec(); } + /** + * Read the reference expressed by the given property. + * + * @param property the reference defining property. Must not be {@literal null}. THe + * @param value the source value identifying to the referenced entity. Must not be {@literal null}. + * @param lookupFunction to execute a lookup query. Must not be {@literal null}. + * @param entityReader the callback to convert raw source values into actual domain types. Must not be + * {@literal null}. + * @return can be {@literal null}. + */ @Nullable - Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, + public Object readReference(MongoPersistentProperty property, Object value, LookupFunction lookupFunction, MongoEntityReader entityReader) { - DocumentReferenceQuery filter = computeFilter(property, value, spELContext); ReferenceCollection referenceCollection = computeReferenceContext(property, value, spELContext); @@ -100,10 +115,12 @@ Object readReference(MongoPersistentProperty property, Object value, LookupFunct private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, SpELContext spELContext) { + // Use the first value as a reference for others in case of collection like if (value instanceof Iterable) { value = ((Iterable) value).iterator().next(); } + // handle DBRef value if (value instanceof DBRef) { return ReferenceCollection.fromDBRef((DBRef) value); } @@ -112,7 +129,7 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop if (value instanceof Document) { - Document ref = (Document) value; + Document documentPointer = (Document) value; if (property.isDocumentReference()) { @@ -120,15 +137,13 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, - () -> ref.get("db", String.class)); + () -> documentPointer.get("db", String.class)); String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, - () -> ref.get("collection", - collection)); + () -> documentPointer.get("collection", collection)); return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceCollection(ref.getString("db"), ref.get("collection", - collection)); + return new ReferenceCollection(documentPointer.getString("db"), documentPointer.get("collection", collection)); } if (property.isDocumentReference()) { @@ -137,16 +152,24 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop DocumentReference documentReference = property.getDocumentReference(); String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null); - String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, - () -> collection); + String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext, () -> collection); return new ReferenceCollection(targetDatabase, targetCollection); } - return new ReferenceCollection(null, - collection); + return new ReferenceCollection(null, collection); } + /** + * Use the given {@link ParameterBindingContext} to compute potential expressions against the value. + * + * @param value must not be {@literal null}. + * @param bindingContext must not be {@literal null}. + * @param defaultValue + * @param + * @return can be {@literal null}. + */ + @Nullable @SuppressWarnings("unchecked") private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier defaultValue) { @@ -154,12 +177,17 @@ private T parseValueOrGet(String value, ParameterBindingContext bindingConte return defaultValue.get(); } + // parameter binding requires a document, since we do not have one, construct it. if (!BsonUtils.isJsonDocument(value) && value.contains("?#{")) { String s = "{ 'target-value' : " + value + "}"; T evaluated = (T) codec.decode(s, bindingContext).get("target-value"); return evaluated != null ? evaluated : defaultValue.get(); } + if (BsonUtils.isJsonDocument(value)) { + return (T) codec.decode(value, bindingContext); + } + T evaluated = (T) bindingContext.evaluateExpression(value); return evaluated != null ? evaluated : defaultValue.get(); } @@ -171,8 +199,8 @@ ParameterBindingContext bindingContext(MongoPersistentProperty property, Object } ValueProvider valueProviderFor(Object source) { - return (index) -> { + return (index) -> { if (source instanceof Document) { return Streamable.of(((Document) source).values()).toList().get(index); } @@ -189,13 +217,24 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object return ctx; } + /** + * Compute the query to retrieve linked documents. + * + * @param property must not be {@literal null}. + * @param value must not be {@literal null}. + * @param spELContext must not be {@literal null}. + * @return never {@literal null}. + */ @SuppressWarnings("unchecked") DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) { - DocumentReference documentReference = property.getDocumentReference(); + DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference() + : ReferenceEmulatingDocumentReference.INSTANCE; + String lookup = documentReference.lookup(); - Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null); + Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), + () -> new Document()); if (property.isCollectionLike() && value instanceof Collection) { @@ -225,45 +264,94 @@ DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object va return new SingleDocumentReferenceQuery(codec.decode(lookup, bindingContext(property, value, spELContext)), sort); } + enum ReferenceEmulatingDocumentReference implements DocumentReference { + + INSTANCE; + + @Override + public Class annotationType() { + return DocumentReference.class; + } + + @Override + public String db() { + return ""; + } + + @Override + public String collection() { + return ""; + } + + @Override + public String lookup() { + return "{ '_id' : ?#{#target} }"; + } + + @Override + public String sort() { + return ""; + } + + @Override + public boolean lazy() { + return false; + } + } + + /** + * {@link DocumentReferenceQuery} implementation fetching a single {@link Document}. + */ static class SingleDocumentReferenceQuery implements DocumentReferenceQuery { - Document filter; - Document sort; + private final Document query; + private final Document sort; + + public SingleDocumentReferenceQuery(Document query, Document sort) { - public SingleDocumentReferenceQuery(Document filter, Document sort) { - this.filter = filter; + this.query = query; this.sort = sort; } @Override - public Bson getFilter() { - return filter; + public Bson getQuery() { + return query; + } + + @Override + public Document getSort() { + return sort; } @Override public Iterable apply(MongoCollection collection) { - Document result = collection.find(getFilter()).limit(1).first(); + Document result = collection.find(getQuery()).sort(getSort()).limit(1).first(); return result != null ? Collections.singleton(result) : Collections.emptyList(); } } + /** + * {@link DocumentReferenceQuery} implementation to retrieve linked {@link Document documents} stored inside a + * {@link Map} structure. Restores the original map order by matching individual query documents against the actual + * values. + */ static class MapDocumentReferenceQuery implements DocumentReferenceQuery { - private final Document filter; + private final Document query; private final Document sort; private final Map filterOrderMap; - public MapDocumentReferenceQuery(Document filter, Document sort, Map filterOrderMap) { + public MapDocumentReferenceQuery(Document query, Document sort, Map filterOrderMap) { - this.filter = filter; + this.query = query; this.sort = sort; this.filterOrderMap = filterOrderMap; } @Override - public Bson getFilter() { - return filter; + public Bson getQuery() { + return query; } @Override @@ -289,33 +377,38 @@ public Iterable restoreOrder(Iterable documents) { } } + /** + * {@link DocumentReferenceQuery} implementation to retrieve linked {@link Document documents} stored inside a + * {@link Collection} like structure. Restores the original order by matching individual query documents against the + * actual values. + */ static class ListDocumentReferenceQuery implements DocumentReferenceQuery { - private final Document filter; + private final Document query; private final Document sort; - public ListDocumentReferenceQuery(Document filter, Document sort) { + public ListDocumentReferenceQuery(Document query, Document sort) { - this.filter = filter; + this.query = query; this.sort = sort; } @Override public Iterable restoreOrder(Iterable documents) { - if (filter.containsKey("$or")) { - List ors = filter.get("$or", List.class); - List target = documents instanceof List ? (List) documents - : Streamable.of(documents).toList(); - return target.stream().sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)) - .collect(Collectors.toList()); + List target = documents instanceof List ? (List) documents + : Streamable.of(documents).toList(); + + if (!sort.isEmpty() || !query.containsKey("$or")) { + return target; } - return documents; + List ors = query.get("$or", List.class); + return target.stream().sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2)).collect(Collectors.toList()); } - public Document getFilter() { - return filter; + public Document getQuery() { + return query; } @Override @@ -339,9 +432,18 @@ int compareAgainstReferenceIndex(List referenceList, Document document } } + /** + * The function that can execute a given {@link DocumentReferenceQuery} within the {@link ReferenceCollection} to + * obtain raw results. + */ @FunctionalInterface interface LookupFunction { + /** + * @param referenceQuery never {@literal null}. + * @param referenceCollection never {@literal null}. + * @return never {@literal null}. + */ Iterable apply(DocumentReferenceQuery referenceQuery, ReferenceCollection referenceCollection); } } 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 index dae2043b4b..91235b5270 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; +import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -23,47 +24,92 @@ import com.mongodb.DBRef; /** + * The {@link ReferenceResolver} allows to load and convert linked entities. + * * @author Christoph Strobl */ public interface ReferenceResolver { + /** + * Resolve the association defined via the given property from a given source value. May deliver a + * {@link LazyLoadingProxy proxy instance} in case of a lazy loading association. + * + * @param property the association defining property. + * @param source the association source value. + * @param referenceLookupDelegate the lookup executing component. + * @param entityReader conversion function capable of constructing entities from raw source. + * @return can be {@literal null}. + */ @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader); - ReferenceLoader getReferenceLoader(); - + /** + * {@link ReferenceCollection} is a value object that contains information about the target database and collection + * name of an association. + */ class ReferenceCollection { - @Nullable + @Nullable // private final String database; private final String collection; + /** + * @param database can be {@literal null} to indicate the configured default + * {@link MongoDatabaseFactory#getMongoDatabase() database} should be used. + * @param collection the target collection name. Must not be {@literal null}. + */ public ReferenceCollection(@Nullable String database, String collection) { - Assert.hasText(collection, "Collection must not be empty or null"); + Assert.hasText(collection, "Collection must not be empty or null!"); this.database = database; this.collection = collection; } - static ReferenceCollection fromDBRef(DBRef dbRef) { + /** + * Create a new instance of {@link ReferenceCollection} from the given {@link DBRef}. + * + * @param dbRef must not be {@literal null}. + * @return new instance of {@link ReferenceCollection}. + */ + public static ReferenceCollection fromDBRef(DBRef dbRef) { return new ReferenceCollection(dbRef.getDatabaseName(), dbRef.getCollectionName()); } + /** + * Get the target collection name. + * + * @return never {@literal null}. + */ public String getCollection() { return collection; } + /** + * Get the target database name. If {@literal null} the default database should be used. + * + * @return can be {@literal null}. + */ @Nullable public String getDatabase() { return database; } } - + /** + * Domain type conversion callback interface that allows to read + */ @FunctionalInterface interface MongoEntityReader { - Object read(Object source, TypeInformation property); + + /** + * Read values from the given source into an object defined via the given {@link TypeInformation}. + * + * @param source never {@literal null}. + * @param typeInformation information abount the desired target type. + * @return never {@literal null}. + */ + Object read(Object source, TypeInformation typeInformation); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index f4bd9436e7..4d51af7dee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -349,7 +349,13 @@ public static String toJson(@Nullable Document source) { * @since 3.0 */ public static boolean isJsonDocument(@Nullable String value) { - return StringUtils.hasText(value) && (value.startsWith("{") && value.endsWith("}")); + + if(!StringUtils.hasText(value)) { + return false; + } + + String potentialJson = value.trim(); + return potentialJson.startsWith("{") && potentialJson.endsWith("}"); } /** 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 index 593e1cd3cf..2c1caf316a 100644 --- 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 @@ -38,6 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils; import org.springframework.data.mongodb.core.mapping.DocumentPointer; @@ -380,6 +381,33 @@ void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() { new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object")); } + @Test // GH-3602 + void useOrderFromAnnotatedSort() { + + String rootCollectionName = template.getCollectionName(CollectionRefRoot.class); + String refCollectionName = template.getCollectionName(SimpleObjectRef.class); + 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 refSource3 = new Document("_id", "ref-3").append("value", "me-the-3-referenced-object"); + Document source = new Document("_id", "id-1").append("value", "v1").append("simpleSortedValueRef", + Arrays.asList("ref-1", "ref-3", "ref-2")); + + template.execute(db -> { + + db.getCollection(refCollectionName).insertOne(refSource1); + db.getCollection(refCollectionName).insertOne(refSource2); + db.getCollection(refCollectionName).insertOne(refSource3); + db.getCollection(rootCollectionName).insertOne(source); + return null; + }); + + CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class); + assertThat(result.getSimpleSortedValueRef()).containsExactly( + new SimpleObjectRef("ref-3", "me-the-3-referenced-object"), + new SimpleObjectRef("ref-2", "me-the-2-referenced-object"), + new SimpleObjectRef("ref-1", "me-the-1-referenced-object")); + } + @Test // GH-3602 void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() { @@ -857,7 +885,8 @@ void updateDerivedMappingFromLookup() { template.save(book); - template.update(Book.class).matching(where("id").is(book.id)).apply(new Update().set("publisher", publisher)).first(); + template.update(Book.class).matching(where("id").is(book.id)).apply(new Update().set("publisher", publisher)) + .first(); Document target = template.execute(db -> { return db.getCollection(template.getCollectionName(Book.class)).find(Filters.eq("_id", book.id)).first(); @@ -890,6 +919,56 @@ void queryDerivedMappingFromLookup() { assertThat(result.publisher).isNotNull(); } + @Test // GH-3602 + void allowsDirectUsageOfAtReference() { + + Publisher publisher = new Publisher(); + publisher.id = "p-1"; + publisher.acronym = "TOR"; + publisher.name = "Tom Doherty Associates"; + + template.save(publisher); + + UsingAtReference root = new UsingAtReference(); + root.id = "book-1"; + root.publisher = publisher; + + template.save(root); + + Document target = template.execute(db -> { + return db.getCollection(template.getCollectionName(UsingAtReference.class)).find(Filters.eq("_id", root.id)).first(); + }); + + assertThat(target).containsEntry("publisher", "p-1"); + + UsingAtReference result = template.findOne(query(where("id").is(root.id)), UsingAtReference.class); + assertThat(result.publisher).isNotNull(); + } + + @Test // GH-3602 + void updateWhenUsingAtReferenceDirectly() { + + Publisher publisher = new Publisher(); + publisher.id = "p-1"; + publisher.acronym = "TOR"; + publisher.name = "Tom Doherty Associates"; + + template.save(publisher); + + UsingAtReference root = new UsingAtReference(); + root.id = "book-1"; + + template.save(root); + template.update(UsingAtReference.class).matching(where("id").is(root.id)).apply(new Update().set("publisher", publisher)).first(); + + Document target = template.execute(db -> { + return db.getCollection(template.getCollectionName(UsingAtReference.class)).find(Filters.eq("_id", root.id)).first(); + }); + + assertThat(target).containsEntry("publisher", "p-1"); + + } + @Data static class SingleRefRoot { @@ -930,6 +1009,9 @@ static class CollectionRefRoot { @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") // List simpleValueRef; + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }", sort = "{ '_id' : -1 } ") // + List simpleSortedValueRef; + @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") // Map mapValueRef; @@ -1051,7 +1133,8 @@ public DocumentPointer convert(SimpleObjectRefWithReadingConverter sou static class WithRefA/* to B */ implements ReferenceAble { @Id String id; - @DocumentReference WithRefB toB; + @DocumentReference // + WithRefB toB; @Override public Object toReference() { @@ -1065,9 +1148,11 @@ public Object toReference() { static class WithRefB/* to A */ implements ReferenceAble { @Id String id; - @DocumentReference(lazy = true) WithRefA lazyToA; + @DocumentReference(lazy = true) // + WithRefA lazyToA; - @DocumentReference WithRefA eagerToA; + @DocumentReference // + WithRefA eagerToA; @Override public Object toReference() { @@ -1091,7 +1176,8 @@ static class Book { String id; - @DocumentReference(lookup = "{ 'acronym' : ?#{acc}, 'name' : ?#{n} }") Publisher publisher; + @DocumentReference(lookup = "{ 'acronym' : ?#{acc}, 'name' : ?#{n} }") // + Publisher publisher; } @@ -1102,4 +1188,13 @@ static class Publisher { String name; } + @Data + static class UsingAtReference { + + String id; + + @Reference // + Publisher publisher; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java new file mode 100644 index 0000000000..6990ddfe88 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactoryUnitTests.java @@ -0,0 +1,139 @@ +/* + * 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.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.mongodb.core.convert.DocumentPointerFactory.LinkageDocument; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; + +/** + * @author Christoph Strobl + */ +public class DocumentPointerFactoryUnitTests { + + @Test // GH-3602 + void errorsOnMongoOperatorUsage() { + + LinkageDocument source = LinkageDocument.from("{ '_id' : { '$eq' : 1 } }"); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> getPointerValue(source, new Book())) // + .withMessageContaining("$eq"); + } + + @Test // GH-3602 + void computesStaticPointer() { + + LinkageDocument source = LinkageDocument.from("{ '_id' : 1 }"); + + assertThat(getPointerValue(source, new Book())).isEqualTo(new Document("_id", 1)); + } + + @Test // GH-3602 + void computesPointerWithIdValuePlaceholder() { + + LinkageDocument source = LinkageDocument.from("{ '_id' : ?#{id} }"); + + assertThat(getPointerValue(source, new Book("book-1", null, null))).isEqualTo(new Document("id", "book-1")); + } + + @Test // GH-3602 + void computesPointerForNonIdValuePlaceholder() { + + LinkageDocument source = LinkageDocument.from("{ 'title' : ?#{book_title} }"); + + assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null))) + .isEqualTo(new Document("book_title", "Living With A Seal")); + } + + @Test // GH-3602 + void computesPlaceholderFromNestedPathValue() { + + LinkageDocument source = LinkageDocument.from("{ 'metadata.pages' : ?#{p} } }"); + + assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null, new Metadata(272)))) + .isEqualTo(new Document("p", 272)); + } + + @Test // GH-3602 + void computesNestedPlaceholderPathValue() { + + LinkageDocument source = LinkageDocument.from("{ 'metadata' : { 'pages' : ?#{metadata.pages} } }"); + + assertThat(getPointerValue(source, new Book("book-1", "Living With A Seal", null, new Metadata(272)))) + .isEqualTo(new Document("metadata", new Document("pages", 272))); + } + + Object getPointerValue(LinkageDocument linkageDocument, Object value) { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(value.getClass()); + return linkageDocument + .getDocumentPointer(mappingContext, persistentEntity, persistentEntity.getPropertyPathAccessor(value)) + .getPointer(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Book { + String id; + String title; + List author; + Metadata metadata; + + public Book(String id, String title, List author) { + this.id = id; + this.title = title; + this.author = author; + } + } + + static class Metadata { + + int pages; + + public Metadata(int pages) { + this.pages = pages; + } + + public int getPages() { + return pages; + } + + public void setPages(int pages) { + this.pages = pages; + } + } + + @Data + static class Author { + String id; + String firstname; + String lastname; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index a3836fd8b3..bd3e98788f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -580,9 +580,9 @@ void writesMapsOfObjectsCorrectly() { org.bson.Document map = (org.bson.Document) field; Object foo = map.get("Foo"); - assertThat(foo).isInstanceOf(BasicDBList.class); + assertThat(foo).isInstanceOf(List.class); - BasicDBList value = (BasicDBList) foo; + List value = (List) foo; assertThat(value.size()).isEqualTo(1); assertThat(value.get(0)).isEqualTo("Bar"); } @@ -695,9 +695,9 @@ void writesPlainMapOfCollectionsCorrectly() { assertThat(result.containsKey("Foo")).isTrue(); assertThat(result.get("Foo")).isNotNull(); - assertThat(result.get("Foo")).isInstanceOf(BasicDBList.class); + assertThat(result.get("Foo")).isInstanceOf(List.class); - BasicDBList list = (BasicDBList) result.get("Foo"); + List list = (List) result.get("Foo"); assertThat(list.size()).isEqualTo(1); assertThat(list.get(0)).isEqualTo(Locale.US.toString()); @@ -744,7 +744,7 @@ void writesArraysAsMapValuesCorrectly() { org.bson.Document map = (org.bson.Document) mapObject; Object valueObject = map.get("foo"); - assertThat(valueObject).isInstanceOf(BasicDBList.class); + assertThat(valueObject).isInstanceOf(List.class); List list = (List) valueObject; assertThat(list.size()).isEqualTo(1); 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 8a462a9370..be9335f2eb 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 @@ -129,10 +129,6 @@ public List bulkFetch(List dbRefs) { return null; } - @Override - public ReferenceLoader getReferenceLoader() { - return null; - } }, context); operations = new ReactiveMongoTemplate(mongoDbFactory, converter); diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 1998fe1ad8..9f9a461ee6 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -902,6 +902,10 @@ It is possible to alter resolution defaults (listed below) via the attributes of | The single document lookup query evaluating placeholders via SpEL expressions using `#target` as the marker for a given source value. `Collection` like or `Map` properties combine individual lookups via an `$or` operator. | An `_id` field based query (`{ '_id' : ?#{#target} }`) using the loaded source value. +| `sort` +| Used for sorting result documents on server side. +| None by default. Result order of `Collection` like properties is restored based on the used lookup query. + | `lazy` | If set to `true` value resolution is delayed upon first access of the property. | Resolves properties eagerly by default. @@ -1182,7 +1186,7 @@ We know it is tempting to use all kinds of MongoDB query operators in the lookup * Mind that resolution takes time and consider a lazy strategy. * A collection of document references is bulk loaded using an `$or` operator. + The original element order is restored in memory which cannot be done when using MongoDB query operators. -In this case Results will be ordered as they are received from the store. +In this case Results will be ordered as they are received from the store or via the provided `@DocumentReference(sort = ...)` attribute. And a few more general remarks: