Enhance support for linking entities.
Add initial support for an alternative to the existing DBRef scenario. The enhancement allows to store and retrieve linked entites via their id or a customizable lookup query. Original pull request: #3647. Closes #3602.
This commit is contained in:
committed by
Mark Paluch
parent
f1354c4508
commit
a51c96298f
@@ -35,7 +35,7 @@ import com.mongodb.DBRef;
|
||||
* @author Mark Paluch
|
||||
* @since 1.4
|
||||
*/
|
||||
public interface DbRefResolver {
|
||||
public interface DbRefResolver extends ReferenceResolver {
|
||||
|
||||
/**
|
||||
* Resolves the given {@link DBRef} into an object of the given {@link MongoPersistentProperty}'s type. The method
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.data.mongodb.ClientSessionException;
|
||||
import org.springframework.data.mongodb.LazyLoadingException;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseUtils;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.objenesis.ObjenesisStd;
|
||||
@@ -67,7 +68,7 @@ import com.mongodb.client.model.Filters;
|
||||
* @author Mark Paluch
|
||||
* @since 1.4
|
||||
*/
|
||||
public class DefaultDbRefResolver implements DbRefResolver {
|
||||
public class DefaultDbRefResolver extends DefaultReferenceResolver implements DbRefResolver, ReferenceResolver {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDbRefResolver.class);
|
||||
|
||||
@@ -82,6 +83,8 @@ public class DefaultDbRefResolver implements DbRefResolver {
|
||||
*/
|
||||
public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) {
|
||||
|
||||
super(new DefaultReferenceLoader(mongoDbFactory));
|
||||
|
||||
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
|
||||
|
||||
this.mongoDbFactory = mongoDbFactory;
|
||||
@@ -114,17 +117,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
|
||||
*/
|
||||
@Override
|
||||
public Document fetch(DBRef dbRef) {
|
||||
|
||||
MongoCollection<Document> mongoCollection = getCollection(dbRef);
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Fetching DBRef '{}' from {}.{}.", dbRef.getId(),
|
||||
StringUtils.hasText(dbRef.getDatabaseName()) ? dbRef.getDatabaseName()
|
||||
: mongoCollection.getNamespace().getDatabaseName(),
|
||||
dbRef.getCollectionName());
|
||||
}
|
||||
|
||||
return mongoCollection.find(Filters.eq("_id", dbRef.getId())).first();
|
||||
return getReferenceLoader().fetch(ReferenceFilter.singleReferenceFilter(Filters.eq("_id", dbRef.getId())), ReferenceContext.fromDBRef(dbRef));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -164,9 +157,9 @@ public class DefaultDbRefResolver implements DbRefResolver {
|
||||
databaseSource.getCollectionName());
|
||||
}
|
||||
|
||||
List<Document> result = mongoCollection //
|
||||
.find(new Document("_id", new Document("$in", ids))) //
|
||||
.into(new ArrayList<>());
|
||||
List<Document> result = getReferenceLoader()
|
||||
.bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ids.stream() //
|
||||
.flatMap(id -> documentWithId(id, result)) //
|
||||
@@ -504,4 +497,10 @@ public class DefaultDbRefResolver implements DbRefResolver {
|
||||
return MongoDatabaseUtils.getDatabase(dbref.getDatabaseName(), mongoDbFactory)
|
||||
.getCollection(dbref.getCollectionName(), Document.class);
|
||||
}
|
||||
|
||||
protected MongoCollection<Document> getCollection(ReferenceContext context) {
|
||||
|
||||
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
|
||||
Document.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseUtils;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class DefaultReferenceLoader implements ReferenceLoader {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultReferenceLoader.class);
|
||||
|
||||
private final MongoDatabaseFactory mongoDbFactory;
|
||||
|
||||
public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) {
|
||||
|
||||
Assert.notNull(mongoDbFactory, "MongoDbFactory translator must not be null!");
|
||||
|
||||
this.mongoDbFactory = mongoDbFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context) {
|
||||
|
||||
MongoCollection<Document> collection = getCollection(context);
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Bulk fetching {} from {}.{}.", filter,
|
||||
StringUtils.hasText(context.getDatabase()) ? context.getDatabase()
|
||||
: collection.getNamespace().getDatabaseName(),
|
||||
context.getCollection());
|
||||
}
|
||||
|
||||
return filter.apply(collection);
|
||||
}
|
||||
|
||||
protected MongoCollection<Document> getCollection(ReferenceContext context) {
|
||||
|
||||
return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
|
||||
Document.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class DefaultReferenceResolver implements ReferenceResolver {
|
||||
|
||||
private final ReferenceLoader referenceLoader;
|
||||
|
||||
public DefaultReferenceResolver(ReferenceLoader referenceLoader) {
|
||||
this.referenceLoader = referenceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReferenceLoader getReferenceLoader() {
|
||||
return referenceLoader;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
|
||||
if (isLazyReference(property)) {
|
||||
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction);
|
||||
}
|
||||
|
||||
return referenceReader.readReference(property, source, lookupFunction);
|
||||
}
|
||||
|
||||
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
|
||||
ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction);
|
||||
}
|
||||
|
||||
protected boolean isLazyReference(MongoPersistentProperty property) {
|
||||
|
||||
if (property.findAnnotation(DocumentReference.class) != null) {
|
||||
return property.findAnnotation(DocumentReference.class).lazy();
|
||||
}
|
||||
|
||||
return property.getDBRef() != null && property.getDBRef().lazy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import static org.springframework.util.ReflectionUtils.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.cglib.proxy.Callback;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.Factory;
|
||||
import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.objenesis.ObjenesisStd;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class LazyLoadingProxyGenerator {
|
||||
|
||||
private final ObjenesisStd objenesis;
|
||||
private final ReferenceReader referenceReader;
|
||||
|
||||
public LazyLoadingProxyGenerator(ReferenceReader referenceReader) {
|
||||
|
||||
this.referenceReader = referenceReader;
|
||||
this.objenesis = new ObjenesisStd(true);
|
||||
}
|
||||
|
||||
public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
|
||||
Class<?> propertyType = property.getType();
|
||||
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction);
|
||||
|
||||
if (!propertyType.isInterface()) {
|
||||
|
||||
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
|
||||
factory.setCallbacks(new Callback[] { interceptor });
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
|
||||
for (Class<?> type : propertyType.getInterfaces()) {
|
||||
proxyFactory.addInterface(type);
|
||||
}
|
||||
|
||||
proxyFactory.addInterface(LazyLoadingProxy.class);
|
||||
proxyFactory.addInterface(propertyType);
|
||||
proxyFactory.addAdvice(interceptor);
|
||||
|
||||
return proxyFactory.getProxy(LazyLoadingProxy.class.getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CGLib enhanced type for the given source type.
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
private Class<?> getEnhancedTypeFor(Class<?> type) {
|
||||
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(type);
|
||||
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
|
||||
enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
|
||||
|
||||
return enhancer.createClass();
|
||||
}
|
||||
|
||||
public static class LazyLoadingInterceptor
|
||||
implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable {
|
||||
|
||||
private final ReferenceReader referenceReader;
|
||||
MongoPersistentProperty property;
|
||||
private volatile boolean resolved;
|
||||
private @org.springframework.lang.Nullable Object result;
|
||||
private Object source;
|
||||
private BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction;
|
||||
|
||||
private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD;
|
||||
|
||||
{
|
||||
try {
|
||||
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
|
||||
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
|
||||
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
|
||||
this.property = property;
|
||||
this.source = source;
|
||||
this.referenceReader = reader;
|
||||
this.lookupFunction = lookupFunction;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
|
||||
return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
|
||||
if (INITIALIZE_METHOD.equals(method)) {
|
||||
return ensureResolved();
|
||||
}
|
||||
|
||||
if (TO_DBREF_METHOD.equals(method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isObjectMethod(method) && Object.class.equals(method.getDeclaringClass())) {
|
||||
|
||||
if (ReflectionUtils.isToStringMethod(method)) {
|
||||
return proxyToString(proxy);
|
||||
}
|
||||
|
||||
if (ReflectionUtils.isEqualsMethod(method)) {
|
||||
return proxyEquals(proxy, args[0]);
|
||||
}
|
||||
|
||||
if (ReflectionUtils.isHashCodeMethod(method)) {
|
||||
return proxyHashCode(proxy);
|
||||
}
|
||||
|
||||
// DATAMONGO-1076 - finalize methods should not trigger proxy initialization
|
||||
if (FINALIZE_METHOD.equals(method)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Object target = ensureResolved();
|
||||
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
private Object ensureResolved() {
|
||||
|
||||
if (!resolved) {
|
||||
this.result = resolve();
|
||||
this.resolved = true;
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
|
||||
private String proxyToString(Object source) {
|
||||
|
||||
StringBuilder description = new StringBuilder();
|
||||
if (source != null) {
|
||||
description.append(source);
|
||||
} else {
|
||||
description.append(System.identityHashCode(source));
|
||||
}
|
||||
description.append("$").append(LazyLoadingProxy.class.getSimpleName());
|
||||
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
private boolean proxyEquals(@org.springframework.lang.Nullable Object proxy, Object that) {
|
||||
|
||||
if (!(that instanceof LazyLoadingProxy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (that == proxy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return proxyToString(proxy).equals(that.toString());
|
||||
}
|
||||
|
||||
private int proxyHashCode(@org.springframework.lang.Nullable Object proxy) {
|
||||
return proxyToString(proxy).hashCode();
|
||||
}
|
||||
|
||||
@org.springframework.lang.Nullable
|
||||
private synchronized Object resolve() {
|
||||
|
||||
if (resolved) {
|
||||
|
||||
// if (LOGGER.isTraceEnabled()) {
|
||||
// LOGGER.trace("Accessing already resolved lazy loading property {}.{}",
|
||||
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// if (LOGGER.isTraceEnabled()) {
|
||||
// LOGGER.trace("Resolving lazy loading property {}.{}",
|
||||
// property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName());
|
||||
// }
|
||||
|
||||
return referenceReader.readReference(property, source, lookupFunction);
|
||||
|
||||
} catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
|
||||
// DataAccessException translatedException = this.exceptionTranslator.translateExceptionIfPossible(ex);
|
||||
//
|
||||
// if (translatedException instanceof ClientSessionException) {
|
||||
// throw new LazyLoadingException("Unable to lazily resolve DBRef! Invalid session state.", ex);
|
||||
// }
|
||||
|
||||
// throw new LazyLoadingException("Unable to lazily resolve DBRef!",
|
||||
// translatedException != null ? translatedException : ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.codecs.Codec;
|
||||
@@ -62,8 +63,10 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator;
|
||||
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.core.mapping.ObjectReference;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty;
|
||||
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
|
||||
@@ -112,6 +115,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
protected final QueryMapper idMapper;
|
||||
protected final DbRefResolver dbRefResolver;
|
||||
protected final DefaultDbRefProxyHandler dbRefProxyHandler;
|
||||
protected final ReferenceReader referenceReader;
|
||||
|
||||
protected @Nullable ApplicationContext applicationContext;
|
||||
protected MongoTypeMapper typeMapper;
|
||||
@@ -136,12 +140,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
Assert.notNull(mappingContext, "MappingContext must not be null!");
|
||||
|
||||
this.dbRefResolver = dbRefResolver;
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
this.typeMapper = new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext,
|
||||
this::getWriteTarget);
|
||||
this.idMapper = new QueryMapper(this);
|
||||
|
||||
|
||||
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
|
||||
this.dbRefProxyHandler = new DefaultDbRefProxyHandler(spELContext, mappingContext,
|
||||
(prop, bson, evaluator, path) -> {
|
||||
@@ -149,6 +153,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
ConversionContext context = getConversionContext(path);
|
||||
return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator);
|
||||
});
|
||||
|
||||
this.referenceReader = new ReferenceReader(mappingContext,
|
||||
(prop, document) -> this.read(prop.getActualType(), document), () -> spELContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,8 +383,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
}
|
||||
|
||||
private <S> S populateProperties(ConversionContext context, MongoPersistentEntity<S> entity,
|
||||
DocumentAccessor documentAccessor,
|
||||
SpELExpressionEvaluator evaluator, S instance) {
|
||||
DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) {
|
||||
|
||||
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
|
||||
conversionService);
|
||||
@@ -423,8 +429,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
@Nullable
|
||||
private Object readIdValue(ConversionContext context, SpELExpressionEvaluator evaluator,
|
||||
MongoPersistentProperty idProperty,
|
||||
Object rawId) {
|
||||
MongoPersistentProperty idProperty, Object rawId) {
|
||||
|
||||
String expression = idProperty.getSpelExpression();
|
||||
Object resolvedValue = expression != null ? evaluator.evaluate(expression) : rawId;
|
||||
@@ -434,8 +439,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
private void readProperties(ConversionContext context, MongoPersistentEntity<?> entity,
|
||||
PersistentPropertyAccessor<?> accessor, DocumentAccessor documentAccessor,
|
||||
MongoDbPropertyValueProvider valueProvider,
|
||||
SpELExpressionEvaluator evaluator) {
|
||||
MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) {
|
||||
|
||||
DbRefResolverCallback callback = null;
|
||||
|
||||
@@ -493,20 +497,38 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback) {
|
||||
|
||||
MongoPersistentProperty property = association.getInverse();
|
||||
Object value = documentAccessor.get(property);
|
||||
final Object value = documentAccessor.get(property);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.isAnnotationPresent(DocumentReference.class)) {
|
||||
|
||||
// quite unusual but sounds like worth having?
|
||||
|
||||
if (conversionService.canConvert(ObjectReference.class, property.getActualType())) {
|
||||
|
||||
// collection like special treatment
|
||||
accessor.setProperty(property, conversionService.convert(new ObjectReference() {
|
||||
@Override
|
||||
public Object getPointer() {
|
||||
return value;
|
||||
}
|
||||
}, property.getActualType()));
|
||||
} else {
|
||||
accessor.setProperty(property, dbRefResolver.resolveReference(property, value, referenceReader));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DBRef dbref = value instanceof DBRef ? (DBRef) value : null;
|
||||
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor,
|
||||
MongoPersistentProperty prop,
|
||||
MongoPersistentEntity<?> unwrappedEntity) {
|
||||
MongoPersistentProperty prop, MongoPersistentEntity<?> unwrappedEntity) {
|
||||
|
||||
if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
|
||||
return read(context, unwrappedEntity, (Document) documentAccessor.getDocument());
|
||||
@@ -725,6 +747,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop.isAssociation()) {
|
||||
|
||||
if (conversionService.canConvert(valueType.getType(), ObjectReference.class)) {
|
||||
accessor.put(prop, conversionService.convert(obj, ObjectReference.class).getPointer());
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
accessor.put(prop, mappingContext.getPersistentEntity(prop.getAssociationTargetType())
|
||||
.getIdentifierAccessor(obj).getIdentifier());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we have a LazyLoadingProxy we make sure it is initialized first.
|
||||
*/
|
||||
@@ -763,6 +797,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (!property.isDbReference()) {
|
||||
|
||||
if (property.isAssociation()) {
|
||||
return writeCollectionInternal(collection.stream().map(it -> {
|
||||
if (conversionService.canConvert(it.getClass(), ObjectReference.class)) {
|
||||
return conversionService.convert(it, ObjectReference.class).getPointer();
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
return mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(it)
|
||||
.getIdentifier();
|
||||
}
|
||||
}).collect(Collectors.toList()), ClassTypeInformation.from(ObjectReference.class), new BasicDBList());
|
||||
}
|
||||
|
||||
if (property.hasExplicitWriteTarget()) {
|
||||
return writeCollectionInternal(collection, new FieldTypeInformation<>(property), new ArrayList<>());
|
||||
}
|
||||
@@ -795,7 +841,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
Assert.notNull(map, "Given map must not be null!");
|
||||
Assert.notNull(property, "PersistentProperty must not be null!");
|
||||
|
||||
if (!property.isDbReference()) {
|
||||
if (!property.isAssociation()) {
|
||||
return writeMapInternal(map, new Document(), property.getTypeInformation());
|
||||
}
|
||||
|
||||
@@ -809,7 +855,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
if (conversions.isSimpleType(key.getClass())) {
|
||||
|
||||
String simpleKey = prepareMapKey(key.toString());
|
||||
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
|
||||
if(property.isDbReference()) {
|
||||
document.put(simpleKey, value != null ? createDBRef(value, property) : null);
|
||||
} else {
|
||||
if (conversionService.canConvert(value.getClass(), ObjectReference.class)) {
|
||||
document.put(simpleKey, conversionService.convert(value, ObjectReference.class).getPointer());
|
||||
} else {
|
||||
// just take the id as a reference
|
||||
document.put(simpleKey, mappingContext.getPersistentEntity(property.getAssociationTargetType()).getIdentifierAccessor(value)
|
||||
.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new MappingException("Cannot use a complex object as a key value.");
|
||||
@@ -1447,8 +1503,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
T target = null;
|
||||
if (document != null) {
|
||||
|
||||
maybeEmitEvent(
|
||||
new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
|
||||
maybeEmitEvent(new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
|
||||
target = (T) readDocument(context, document, type);
|
||||
}
|
||||
|
||||
@@ -1541,9 +1596,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private <T extends Object> T doConvert(Object value, Class<? extends T> target, @Nullable Class<? extends T> fallback) {
|
||||
private <T extends Object> T doConvert(Object value, Class<? extends T> target,
|
||||
@Nullable Class<? extends T> fallback) {
|
||||
|
||||
if(conversionService.canConvert(value.getClass(), target) || fallback == null) {
|
||||
if (conversionService.canConvert(value.getClass(), target) || fallback == null) {
|
||||
return conversionService.convert(value, target);
|
||||
}
|
||||
return conversionService.convert(value, fallback);
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -69,4 +73,16 @@ public enum NoOpDbRefResolver implements DbRefResolver {
|
||||
private <T> T handle() throws UnsupportedOperationException {
|
||||
throw new UnsupportedOperationException("DBRef resolution is not supported!");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReferenceLoader getReferenceLoader() {
|
||||
return handle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface ReferenceLoader {
|
||||
|
||||
@Nullable
|
||||
default Document fetch(ReferenceFilter filter, ReferenceContext context) {
|
||||
return bulkFetch(filter, context).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context);
|
||||
|
||||
interface ReferenceFilter {
|
||||
|
||||
Bson getFilter();
|
||||
|
||||
default Bson getSort() {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
default Stream<Document> apply(MongoCollection<Document> collection) {
|
||||
return restoreOrder(StreamSupport.stream(collection.find(getFilter()).sort(getSort()).spliterator(), false));
|
||||
}
|
||||
|
||||
default Stream<Document> restoreOrder(Stream<Document> stream) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
static ReferenceFilter referenceFilter(Bson bson) {
|
||||
return () -> bson;
|
||||
}
|
||||
|
||||
static ReferenceFilter singleReferenceFilter(Bson bson) {
|
||||
|
||||
return new ReferenceFilter() {
|
||||
|
||||
@Override
|
||||
public Bson getFilter() {
|
||||
return bson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> apply(MongoCollection<Document> collection) {
|
||||
|
||||
Document result = collection.find(getFilter()).sort(getSort()).limit(1).first();
|
||||
return result != null ? Stream.of(result) : Stream.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.SpELContext;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceContext;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
|
||||
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
|
||||
import org.springframework.data.mongodb.util.json.ValueProvider;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.mongodb.DBRef;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class ReferenceReader {
|
||||
|
||||
private final ParameterBindingDocumentCodec codec;
|
||||
|
||||
private final Lazy<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContext;
|
||||
private final BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction;
|
||||
private final Supplier<SpELContext> spelContextSupplier;
|
||||
|
||||
public ReferenceReader(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
|
||||
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
|
||||
Supplier<SpELContext> spelContextSupplier) {
|
||||
|
||||
this(() -> mappingContext, documentConversionFunction, spelContextSupplier);
|
||||
}
|
||||
|
||||
public ReferenceReader(
|
||||
Supplier<MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty>> mappingContextSupplier,
|
||||
BiFunction<MongoPersistentProperty, Document, Object> documentConversionFunction,
|
||||
Supplier<SpELContext> spelContextSupplier) {
|
||||
|
||||
this.mappingContext = Lazy.of(mappingContextSupplier);
|
||||
this.documentConversionFunction = documentConversionFunction;
|
||||
this.spelContextSupplier = spelContextSupplier;
|
||||
this.codec = new ParameterBindingDocumentCodec();
|
||||
}
|
||||
|
||||
Object readReference(MongoPersistentProperty property, Object value,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
|
||||
SpELContext spELContext = spelContextSupplier.get();
|
||||
|
||||
ReferenceFilter filter = computeFilter(property, value, spELContext);
|
||||
ReferenceContext referenceContext = computeReferenceContext(property, value, spELContext);
|
||||
|
||||
Stream<Document> result = lookupFunction.apply(referenceContext, filter);
|
||||
|
||||
if (property.isCollectionLike()) {
|
||||
return result.map(it -> documentConversionFunction.apply(property, it)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (property.isMap()) {
|
||||
|
||||
// the order is a real problem here
|
||||
Iterator<Object> keyIterator = ((Map) value).keySet().iterator();
|
||||
return result.map(it -> it.entrySet().stream().collect(Collectors.toMap(key -> key.getKey(), val -> {
|
||||
Object apply = documentConversionFunction.apply(property, (Document) val.getValue());
|
||||
return apply;
|
||||
}))).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
return result.map(it -> documentConversionFunction.apply(property, it)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private ReferenceContext computeReferenceContext(MongoPersistentProperty property, Object value,
|
||||
SpELContext spELContext) {
|
||||
|
||||
if (value instanceof Iterable) {
|
||||
value = ((Iterable<?>) value).iterator().next();
|
||||
}
|
||||
|
||||
if (value instanceof DBRef) {
|
||||
return ReferenceContext.fromDBRef((DBRef) value);
|
||||
}
|
||||
|
||||
if (value instanceof Document) {
|
||||
|
||||
Document ref = (Document) value;
|
||||
|
||||
if (property.isAnnotationPresent(DocumentReference.class)) {
|
||||
|
||||
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
|
||||
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext,
|
||||
() -> ref.get("db", String.class));
|
||||
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
|
||||
() -> ref.get("collection",
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
|
||||
return new ReferenceContext(targetDatabase, targetCollection);
|
||||
}
|
||||
|
||||
return new ReferenceContext(ref.getString("db"), ref.get("collection",
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection()));
|
||||
}
|
||||
|
||||
if (property.isAnnotationPresent(DocumentReference.class)) {
|
||||
|
||||
ParameterBindingContext bindingContext = bindingContext(property, value, spELContext);
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
|
||||
String targetDatabase = parseValueOrGet(documentReference.db(), bindingContext, () -> null);
|
||||
String targetCollection = parseValueOrGet(documentReference.collection(), bindingContext,
|
||||
() -> mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
|
||||
Document sort = parseValueOrGet(documentReference.sort(), bindingContext, () -> null);
|
||||
|
||||
return new ReferenceContext(targetDatabase, targetCollection);
|
||||
}
|
||||
|
||||
return new ReferenceContext(null,
|
||||
mappingContext.get().getPersistentEntity(property.getAssociationTargetType()).getCollection());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<T> defaultValue) {
|
||||
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return defaultValue.get();
|
||||
}
|
||||
|
||||
if (!BsonUtils.isJsonDocument(value) && value.contains("?#{")) {
|
||||
String s = "{ 'target-value' : " + value + "}";
|
||||
T evaluated = (T) new ParameterBindingDocumentCodec().decode(s, bindingContext).get("target-value ");
|
||||
return evaluated != null ? evaluated : defaultValue.get();
|
||||
}
|
||||
|
||||
T evaluated = (T) bindingContext.evaluateExpression(value);
|
||||
return evaluated != null ? evaluated : defaultValue.get();
|
||||
}
|
||||
|
||||
ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) {
|
||||
|
||||
return new ParameterBindingContext(valueProviderFor(source), spELContext.getParser(),
|
||||
() -> evaluationContextFor(property, source, spELContext));
|
||||
}
|
||||
|
||||
ValueProvider valueProviderFor(Object source) {
|
||||
return (index) -> {
|
||||
|
||||
if (source instanceof Document) {
|
||||
return Streamable.of(((Document) source).values()).toList().get(index);
|
||||
}
|
||||
return source;
|
||||
};
|
||||
}
|
||||
|
||||
EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) {
|
||||
|
||||
EvaluationContext ctx = spELContext.getEvaluationContext(source);
|
||||
ctx.setVariable("target", source);
|
||||
ctx.setVariable(property.getName(), source);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ReferenceFilter computeFilter(MongoPersistentProperty property, Object value, SpELContext spELContext) {
|
||||
|
||||
DocumentReference documentReference = property.getRequiredAnnotation(DocumentReference.class);
|
||||
String lookup = documentReference.lookup();
|
||||
|
||||
Document sort = parseValueOrGet(documentReference.sort(), bindingContext(property, value, spELContext), () -> null);
|
||||
|
||||
if (property.isCollectionLike() && value instanceof Collection) {
|
||||
|
||||
List<Document> ors = new ArrayList<>();
|
||||
for (Object entry : (Collection) value) {
|
||||
|
||||
Document decoded = codec.decode(lookup, bindingContext(property, entry, spELContext));
|
||||
ors.add(decoded);
|
||||
}
|
||||
|
||||
return new ListReferenceFilter(new Document("$or", ors), sort);
|
||||
}
|
||||
|
||||
if (property.isMap() && value instanceof Map) {
|
||||
|
||||
Map<Object, Document> filterMap = new LinkedHashMap<>();
|
||||
|
||||
for (Entry entry : ((Map<Object, Object>) value).entrySet()) {
|
||||
|
||||
Document decoded = codec.decode(lookup, bindingContext(property, entry.getValue(), spELContext));
|
||||
filterMap.put(entry.getKey(), decoded);
|
||||
}
|
||||
|
||||
return new MapReferenceFilter(new Document("$or", filterMap.values()), sort, filterMap);
|
||||
}
|
||||
|
||||
return new SingleReferenceFilter(codec.decode(lookup, bindingContext(property, value, spELContext)), sort);
|
||||
}
|
||||
|
||||
static class SingleReferenceFilter implements ReferenceFilter {
|
||||
|
||||
Document filter;
|
||||
Document sort;
|
||||
|
||||
public SingleReferenceFilter(Document filter, Document sort) {
|
||||
this.filter = filter;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bson getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> apply(MongoCollection<Document> collection) {
|
||||
|
||||
Document result = collection.find(getFilter()).limit(1).first();
|
||||
return result != null ? Stream.of(result) : Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
static class MapReferenceFilter implements ReferenceFilter {
|
||||
|
||||
Document filter;
|
||||
Document sort;
|
||||
Map<Object, Document> filterOrderMap;
|
||||
|
||||
public MapReferenceFilter(Document filter, Document sort, Map<Object, Document> filterOrderMap) {
|
||||
|
||||
this.filter = filter;
|
||||
this.filterOrderMap = filterOrderMap;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bson getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bson getSort() {
|
||||
return sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> restoreOrder(Stream<Document> stream) {
|
||||
|
||||
Map<String, Object> targetMap = new LinkedHashMap<>();
|
||||
List<Document> collected = stream.collect(Collectors.toList());
|
||||
|
||||
for (Entry<Object, Document> filterMapping : filterOrderMap.entrySet()) {
|
||||
|
||||
String key = filterMapping.getKey().toString();
|
||||
Optional<Document> first = collected.stream().filter(it -> {
|
||||
|
||||
boolean found = it.entrySet().containsAll(filterMapping.getValue().entrySet());
|
||||
return found;
|
||||
}).findFirst();
|
||||
|
||||
targetMap.put(key, first.orElse(null));
|
||||
}
|
||||
return Stream.of(new Document(targetMap));
|
||||
}
|
||||
}
|
||||
|
||||
static class ListReferenceFilter implements ReferenceFilter {
|
||||
|
||||
Document filter;
|
||||
Document sort;
|
||||
|
||||
public ListReferenceFilter(Document filter, Document sort) {
|
||||
this.filter = filter;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> restoreOrder(Stream<Document> stream) {
|
||||
|
||||
if (filter.containsKey("$or")) {
|
||||
List<Document> ors = filter.get("$or", List.class);
|
||||
return stream.sorted((o1, o2) -> compareAgainstReferenceIndex(ors, o1, o2));
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
public Document getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document getSort() {
|
||||
return sort;
|
||||
}
|
||||
|
||||
int compareAgainstReferenceIndex(List<Document> referenceList, Document document1, Document document2) {
|
||||
|
||||
for (int i = 0; i < referenceList.size(); i++) {
|
||||
|
||||
Set<Entry<String, Object>> entries = referenceList.get(i).entrySet();
|
||||
if (document1.entrySet().containsAll(entries)) {
|
||||
return -1;
|
||||
}
|
||||
if (document2.entrySet().containsAll(entries)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return referenceList.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.DBRef;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface ReferenceResolver {
|
||||
|
||||
@Nullable
|
||||
Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
|
||||
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction);
|
||||
|
||||
default Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader) {
|
||||
return resolveReference(property, source, referenceReader, (ctx, filter) -> {
|
||||
if (property.isCollectionLike() || property.isMap()) {
|
||||
return getReferenceLoader().bulkFetch(filter, ctx);
|
||||
}
|
||||
Object target = getReferenceLoader().fetch(filter, ctx);
|
||||
return target == null ? Stream.empty() : Stream.of(getReferenceLoader().fetch(filter, ctx));
|
||||
});
|
||||
}
|
||||
|
||||
ReferenceLoader getReferenceLoader();
|
||||
|
||||
class ReferenceContext {
|
||||
|
||||
@Nullable final String database;
|
||||
final String collection;
|
||||
|
||||
public ReferenceContext(@Nullable String database, String collection) {
|
||||
|
||||
this.database = database;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
static ReferenceContext fromDBRef(DBRef dbRef) {
|
||||
return new ReferenceContext(dbRef.getDatabaseName(), dbRef.getCollectionName());
|
||||
}
|
||||
|
||||
public String getCollection() {
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getDatabase() {
|
||||
return database;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.mapping;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ObjectReference<T> {
|
||||
T getPointer();
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.mongodb.core.convert.LazyLoadingTestUtils;
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef;
|
||||
import org.springframework.data.mongodb.core.mapping.DocumentReference;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.ObjectReference;
|
||||
import org.springframework.data.mongodb.test.util.Client;
|
||||
import org.springframework.data.mongodb.test.util.MongoClientExtension;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.model.Filters;
|
||||
|
||||
/**
|
||||
* {@link DBRef} related integration tests for {@link MongoTemplate}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MongoClientExtension.class)
|
||||
public class MongoTemplateDocumentReferenceTests {
|
||||
|
||||
public static final String DB_NAME = "manual-reference-tests";
|
||||
|
||||
static @Client MongoClient client;
|
||||
|
||||
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
|
||||
|
||||
cfg.configureDatabaseFactory(it -> {
|
||||
|
||||
it.client(client);
|
||||
it.defaultDb(DB_NAME);
|
||||
});
|
||||
|
||||
cfg.configureConversion(it -> {
|
||||
it.customConverters(new ReferencableConverter());
|
||||
});
|
||||
|
||||
cfg.configureMappingContext(it -> {
|
||||
it.autocreateIndex(false);
|
||||
});
|
||||
});
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
template.flushDatabase();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeSimpleTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
|
||||
SingleRefRoot source = new SingleRefRoot();
|
||||
source.id = "root-1";
|
||||
source.simpleValueRef = new SimpleObjectRef("ref-1", "me-the-referenced-object");
|
||||
|
||||
template.save(source);
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
assertThat(target.get("simpleValueRef")).isEqualTo("ref-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeMapTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
|
||||
|
||||
CollectionRefRoot source = new CollectionRefRoot();
|
||||
source.id = "root-1";
|
||||
source.mapValueRef = new LinkedHashMap<>();
|
||||
source.mapValueRef.put("frodo", new SimpleObjectRef("ref-1", "me-the-1-referenced-object"));
|
||||
source.mapValueRef.put("bilbo", new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
|
||||
template.save(source);
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
System.out.println("target: " + target.toJson());
|
||||
assertThat(target.get("mapValueRef", Map.class)).containsEntry("frodo", "ref-1").containsEntry("bilbo", "ref-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCollectionOfSimpleTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
|
||||
CollectionRefRoot source = new CollectionRefRoot();
|
||||
source.id = "root-1";
|
||||
source.simpleValueRef = Arrays.asList(new SimpleObjectRef("ref-1", "me-the-1-referenced-object"),
|
||||
new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
|
||||
template.save(source);
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
assertThat(target.get("simpleValueRef", List.class)).containsExactly("ref-1", "ref-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
|
||||
SingleRefRoot source = new SingleRefRoot();
|
||||
source.id = "root-1";
|
||||
source.objectValueRef = new ObjectRefOfDocument("ref-1", "me-the-referenced-object");
|
||||
|
||||
template.save(source);
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
assertThat(target.get("objectValueRef")).isEqualTo(source.getObjectValueRef().toReference());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCollectionOfObjectTypeReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
|
||||
CollectionRefRoot source = new CollectionRefRoot();
|
||||
source.id = "root-1";
|
||||
source.objectValueRef = Arrays.asList(new ObjectRefOfDocument("ref-1", "me-the-1-referenced-object"),
|
||||
new ObjectRefOfDocument("ref-2", "me-the-2-referenced-object"));
|
||||
|
||||
template.save(source);
|
||||
|
||||
Document target = template.execute(db -> {
|
||||
return db.getCollection(rootCollectionName).find(Filters.eq("_id", "root-1")).first();
|
||||
});
|
||||
|
||||
assertThat(target.get("objectValueRef", List.class)).containsExactly(
|
||||
source.getObjectValueRef().get(0).toReference(), source.getObjectValueRef().get(1).toReference());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readSimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef", "ref-1");
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
assertThat(result.getSimpleValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCollectionOfSimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleValueRef",
|
||||
Collections.singletonList("ref-1"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getSimpleValueRef()).containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readLazySimpleTypeObjectReference() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simpleLazyValueRef", "ref-1");
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
|
||||
LazyLoadingTestUtils.assertProxy(result.simpleLazyValueRef, (proxy) -> {
|
||||
|
||||
assertThat(proxy.isResolved()).isFalse();
|
||||
assertThat(proxy.currentValue()).isNull();
|
||||
});
|
||||
assertThat(result.getSimpleLazyValueRef()).isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readSimpleTypeObjectReferenceFromFieldWithCustomName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
|
||||
"ref-1");
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
assertThat(result.getSimpleValueRefWithAnnotatedFieldName())
|
||||
.isEqualTo(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCollectionTypeObjectReferenceFromFieldWithCustomName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("simple-value-ref-annotated-field-name",
|
||||
Collections.singletonList("ref-1"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getSimpleValueRefWithAnnotatedFieldName())
|
||||
.containsExactly(new SimpleObjectRef("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readObjectReferenceFromDocumentType() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
|
||||
new Document("id", "ref-1").append("property", "without-any-meaning"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
assertThat(result.getObjectValueRef()).isEqualTo(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCollectionObjectReferenceFromDocumentType() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(ObjectRefOfDocument.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRef",
|
||||
Collections.singletonList(new Document("id", "ref-1").append("property", "without-any-meaning")));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getObjectValueRef())
|
||||
.containsExactly(new ObjectRefOfDocument("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readObjectReferenceFromDocumentDeclaringCollectionName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
|
||||
Document refSource = new Document("_id", "ref-1").append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append(
|
||||
"objectValueRefWithEmbeddedCollectionName",
|
||||
new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
|
||||
.append("property", "without-any-meaning"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
assertThat(result.getObjectValueRefWithEmbeddedCollectionName())
|
||||
.isEqualTo(new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCollectionObjectReferenceFromDocumentDeclaringCollectionName() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = "object-ref-of-document-with-embedded-collection-name";
|
||||
Document refSource1 = new Document("_id", "ref-1").append("value", "me-the-1-referenced-object");
|
||||
Document refSource2 = new Document("_id", "ref-2").append("value", "me-the-2-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append(
|
||||
"objectValueRefWithEmbeddedCollectionName",
|
||||
Arrays.asList(
|
||||
new Document("id", "ref-2").append("collection", "object-ref-of-document-with-embedded-collection-name"),
|
||||
new Document("id", "ref-1").append("collection", "object-ref-of-document-with-embedded-collection-name")
|
||||
.append("property", "without-any-meaning")));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource1);
|
||||
db.getCollection(refCollectionName).insertOne(refSource2);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getObjectValueRefWithEmbeddedCollectionName()).containsExactly(
|
||||
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-2", "me-the-2-referenced-object"),
|
||||
new ObjectRefOfDocumentWithEmbeddedCollectionName("ref-1", "me-the-1-referenced-object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
|
||||
.append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
|
||||
new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
assertThat(result.getObjectValueRefOnNonIdFields())
|
||||
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readLazyObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(SingleRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
|
||||
.append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("lazyObjectValueRefOnNonIdFields",
|
||||
new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property", "without-any-meaning"));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
SingleRefRoot result = template.findOne(query(where("id").is("id-1")), SingleRefRoot.class);
|
||||
|
||||
LazyLoadingTestUtils.assertProxy(result.lazyObjectValueRefOnNonIdFields, (proxy) -> {
|
||||
|
||||
assertThat(proxy.isResolved()).isFalse();
|
||||
assertThat(proxy.currentValue()).isNull();
|
||||
});
|
||||
assertThat(result.getLazyObjectValueRefOnNonIdFields())
|
||||
.isEqualTo(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCollectionObjectReferenceFromDocumentNotRelatingToTheIdProperty() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(ObjectRefOnNonIdField.class);
|
||||
Document refSource = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
|
||||
.append("value", "me-the-referenced-object");
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("objectValueRefOnNonIdFields",
|
||||
Collections.singletonList(new Document("refKey1", "ref-key-1").append("refKey2", "ref-key-2").append("property",
|
||||
"without-any-meaning")));
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(refCollectionName).insertOne(refSource);
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
assertThat(result.getObjectValueRefOnNonIdFields())
|
||||
.containsExactly(new ObjectRefOnNonIdField("ref-1", "me-the-referenced-object", "ref-key-1", "ref-key-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readMapOfReferences() {
|
||||
|
||||
String rootCollectionName = template.getCollectionName(CollectionRefRoot.class);
|
||||
String refCollectionName = template.getCollectionName(SimpleObjectRef.class);
|
||||
|
||||
Document refSource1 = new Document("_id", "ref-1").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
|
||||
.append("value", "me-the-1-referenced-object");
|
||||
|
||||
Document refSource2 = new Document("_id", "ref-2").append("refKey1", "ref-key-1").append("refKey2", "ref-key-2")
|
||||
.append("value", "me-the-2-referenced-object");
|
||||
|
||||
Map<String, String> refmap = new LinkedHashMap<>();
|
||||
refmap.put("frodo", "ref-1");
|
||||
refmap.put("bilbo", "ref-2");
|
||||
|
||||
Document source = new Document("_id", "id-1").append("value", "v1").append("mapValueRef", refmap);
|
||||
|
||||
template.execute(db -> {
|
||||
|
||||
db.getCollection(rootCollectionName).insertOne(source);
|
||||
db.getCollection(refCollectionName).insertOne(refSource1);
|
||||
db.getCollection(refCollectionName).insertOne(refSource2);
|
||||
return null;
|
||||
});
|
||||
|
||||
CollectionRefRoot result = template.findOne(query(where("id").is("id-1")), CollectionRefRoot.class);
|
||||
System.out.println("result: " + result);
|
||||
|
||||
assertThat(result.getMapValueRef()).containsEntry("frodo",
|
||||
new SimpleObjectRef("ref-1", "me-the-1-referenced-object"))
|
||||
.containsEntry("bilbo",
|
||||
new SimpleObjectRef("ref-2", "me-the-2-referenced-object"));
|
||||
}
|
||||
|
||||
@Data
|
||||
static class SingleRefRoot {
|
||||
|
||||
String id;
|
||||
String value;
|
||||
|
||||
@DocumentReference SimpleObjectRefWithReadingConverter withReadingConverter;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
|
||||
SimpleObjectRef simpleValueRef;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }", lazy = true) //
|
||||
SimpleObjectRef simpleLazyValueRef;
|
||||
|
||||
@Field("simple-value-ref-annotated-field-name") //
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
|
||||
SimpleObjectRef simpleValueRefWithAnnotatedFieldName;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
|
||||
ObjectRefOfDocument objectValueRef;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "#collection") //
|
||||
ObjectRefOfDocumentWithEmbeddedCollectionName objectValueRefWithEmbeddedCollectionName;
|
||||
|
||||
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
|
||||
ObjectRefOnNonIdField objectValueRefOnNonIdFields;
|
||||
|
||||
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }", lazy = true) //
|
||||
ObjectRefOnNonIdField lazyObjectValueRefOnNonIdFields;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class CollectionRefRoot {
|
||||
|
||||
String id;
|
||||
String value;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
|
||||
List<SimpleObjectRef> simpleValueRef;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
|
||||
Map<String, SimpleObjectRef> mapValueRef;
|
||||
|
||||
@Field("simple-value-ref-annotated-field-name") //
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") //
|
||||
List<SimpleObjectRef> simpleValueRefWithAnnotatedFieldName;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{id}' }") //
|
||||
List<ObjectRefOfDocument> objectValueRef;
|
||||
|
||||
@DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") //
|
||||
List<ObjectRefOfDocumentWithEmbeddedCollectionName> objectValueRefWithEmbeddedCollectionName;
|
||||
|
||||
@DocumentReference(lookup = "{ 'refKey1' : '?#{refKey1}', 'refKey2' : '?#{refKey2}' }") //
|
||||
List<ObjectRefOnNonIdField> objectValueRefOnNonIdFields;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ReferenceAble {
|
||||
Object toReference();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@org.springframework.data.mongodb.core.mapping.Document("simple-object-ref")
|
||||
static class SimpleObjectRef {
|
||||
|
||||
@Id String id;
|
||||
String value;
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
static class SimpleObjectRefWithReadingConverter extends SimpleObjectRef {
|
||||
|
||||
public SimpleObjectRefWithReadingConverter(String id, String value, String id1, String value1) {
|
||||
super(id, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class ObjectRefOfDocument implements ReferenceAble {
|
||||
|
||||
@Id String id;
|
||||
String value;
|
||||
|
||||
@Override
|
||||
public Object toReference() {
|
||||
return new Document("id", id).append("property", "without-any-meaning");
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class ObjectRefOfDocumentWithEmbeddedCollectionName implements ReferenceAble {
|
||||
|
||||
@Id String id;
|
||||
String value;
|
||||
|
||||
@Override
|
||||
public Object toReference() {
|
||||
return new Document("id", id).append("collection", "object-ref-of-document-with-embedded-collection-name");
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class ObjectRefOnNonIdField implements ReferenceAble {
|
||||
|
||||
@Id String id;
|
||||
String value;
|
||||
String refKey1;
|
||||
String refKey2;
|
||||
|
||||
@Override
|
||||
public Object toReference() {
|
||||
return new Document("refKey1", refKey1).append("refKey2", refKey2);
|
||||
}
|
||||
}
|
||||
|
||||
static class ReferencableConverter implements Converter<ReferenceAble, ObjectReference> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ObjectReference convert(ReferenceAble source) {
|
||||
return source::toReference;
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
class DocumentToSimpleObjectRefWithReadingConverter
|
||||
implements Converter<ObjectReference<Document>, SimpleObjectRefWithReadingConverter> {
|
||||
|
||||
private final MongoTemplate template;
|
||||
|
||||
public DocumentToSimpleObjectRefWithReadingConverter(MongoTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SimpleObjectRefWithReadingConverter convert(ObjectReference<Document> source) {
|
||||
return template.findOne(query(where("id").is(source.getPointer().get("the-ref-key-you-did-not-expect"))),
|
||||
SimpleObjectRefWithReadingConverter.class);
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
class SimpleObjectRefWithReadingConverterToDocumentConverter
|
||||
implements Converter<SimpleObjectRefWithReadingConverter, ObjectReference<Document>> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ObjectReference<Document> convert(SimpleObjectRefWithReadingConverter source) {
|
||||
return () -> new Document("the-ref-key-you-did-not-expect", source.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,8 @@ public class DbRefMappingMongoConverterUnitTests {
|
||||
when(dbMock.getCollection(anyString(), eq(Document.class))).thenReturn(collectionMock);
|
||||
|
||||
FindIterable fi = mock(FindIterable.class);
|
||||
when(fi.limit(anyInt())).thenReturn(fi);
|
||||
when(fi.sort(any())).thenReturn(fi);
|
||||
when(fi.first()).thenReturn(mapValDocument);
|
||||
when(collectionMock.find(Mockito.any(Bson.class))).thenReturn(fi);
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.DocumentTestUtils;
|
||||
@@ -65,6 +64,8 @@ class DefaultDbRefResolverUnitTests {
|
||||
when(factoryMock.getMongoDatabase()).thenReturn(dbMock);
|
||||
when(dbMock.getCollection(anyString(), any(Class.class))).thenReturn(collectionMock);
|
||||
when(collectionMock.find(any(Document.class))).thenReturn(cursorMock);
|
||||
when(cursorMock.sort(any(Document.class))).thenReturn(cursorMock);
|
||||
when(cursorMock.spliterator()).thenReturn(Collections.<Document> emptyList().spliterator());
|
||||
|
||||
resolver = new DefaultDbRefResolver(factoryMock);
|
||||
}
|
||||
@@ -115,7 +116,7 @@ class DefaultDbRefResolverUnitTests {
|
||||
DBRef ref1 = new DBRef("collection-1", o1.get("_id"));
|
||||
DBRef ref2 = new DBRef("collection-1", o2.get("_id"));
|
||||
|
||||
when(cursorMock.into(any())).then(invocation -> Arrays.asList(o2, o1));
|
||||
when(cursorMock.spliterator()).thenReturn(Arrays.asList(o2, o1).spliterator());
|
||||
|
||||
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(o1, o2);
|
||||
}
|
||||
@@ -128,7 +129,7 @@ class DefaultDbRefResolverUnitTests {
|
||||
DBRef ref1 = new DBRef("collection-1", document.get("_id"));
|
||||
DBRef ref2 = new DBRef("collection-1", document.get("_id"));
|
||||
|
||||
when(cursorMock.into(any())).then(invocation -> Arrays.asList(document));
|
||||
when(cursorMock.spliterator()).thenReturn(Arrays.asList(document).spliterator());
|
||||
|
||||
assertThat(resolver.bulkFetch(Arrays.asList(ref1, ref2))).containsExactly(document, document);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.cglib.proxy.Factory;
|
||||
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.LazyLoadingInterceptor;
|
||||
import org.springframework.data.mongodb.core.mapping.Unwrapped;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
@@ -49,8 +52,35 @@ public class LazyLoadingTestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertProxy(Object proxy, Consumer<LazyLoadingProxyValueRetriever> verification) {
|
||||
|
||||
LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor = (LazyLoadingProxyGenerator.LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice()
|
||||
: ((Factory) proxy).getCallback(0));
|
||||
|
||||
verification.accept(new LazyLoadingProxyValueRetriever(interceptor));
|
||||
}
|
||||
|
||||
private static LazyLoadingInterceptor extractInterceptor(Object proxy) {
|
||||
return (LazyLoadingInterceptor) (proxy instanceof Advised ? ((Advised) proxy).getAdvisors()[0].getAdvice()
|
||||
: ((Factory) proxy).getCallback(0));
|
||||
}
|
||||
|
||||
public static class LazyLoadingProxyValueRetriever {
|
||||
|
||||
LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor;
|
||||
|
||||
public LazyLoadingProxyValueRetriever(LazyLoadingProxyGenerator.LazyLoadingInterceptor interceptor) {
|
||||
this.interceptor = interceptor;
|
||||
}
|
||||
|
||||
public boolean isResolved() {
|
||||
return (boolean) ReflectionTestUtils.getField(interceptor, "resolved");
|
||||
}
|
||||
|
||||
@Unwrapped.Nullable
|
||||
public Object currentValue() {
|
||||
return ReflectionTestUtils.getField(interceptor, "result");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,21 @@ package org.springframework.data.mongodb.performance;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceLoader.ReferenceFilter;
|
||||
import org.springframework.data.mongodb.core.convert.ReferenceReader;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
@@ -96,6 +104,13 @@ public class ReactivePerformanceTests {
|
||||
context.afterPropertiesSet();
|
||||
|
||||
converter = new MappingMongoConverter(new DbRefResolver() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
|
||||
DbRefProxyHandler proxyHandler) {
|
||||
@@ -117,6 +132,11 @@ public class ReactivePerformanceTests {
|
||||
public List<Document> bulkFetch(List<DBRef> dbRefs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReferenceLoader getReferenceLoader() {
|
||||
return null;
|
||||
}
|
||||
}, context);
|
||||
operations = new ReactiveMongoTemplate(mongoDbFactory, converter);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<logger name="org.springframework.data.mongodb.core" level="error"/>
|
||||
<logger name="org.springframework.data.mongodb.test.util" level="info"/>
|
||||
<!-- <logger name="org.springframework.data.mongodb.core.convert" level="trace"/>-->
|
||||
|
||||
<root level="error">
|
||||
<appender-ref ref="console" />
|
||||
|
||||
Reference in New Issue
Block a user