Skip to content

Commit

Permalink
Update entity linking support to derive document pointer from lookup …
Browse files Browse the repository at this point in the history
…query.

Simplify usage by computing the pointer from the lookup.
Update the reference documentation, add JavaDoc and refine API.

Original pull request: #3647.
Closes #3602.
  • Loading branch information
christophstrobl authored and mp911de committed May 21, 2021
1 parent 48ac7e7 commit 6ed274b
Show file tree
Hide file tree
Showing 27 changed files with 1,468 additions and 247 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

/*
Expand Down Expand Up @@ -157,9 +158,9 @@ public List<Document> bulkFetch(List<DBRef> refs) {
databaseSource.getCollectionName());
}

List<Document> result = getReferenceLoader()
.bulkFetch(ReferenceFilter.referenceFilter(new Document("_id", new Document("$in", ids))), ReferenceContext.fromDBRef(refs.iterator().next()))
.collect(Collectors.toList());
List<Document> result = mongoCollection //
.find(new Document("_id", new Document("$in", ids))) //
.into(new ArrayList<>());

return ids.stream() //
.flatMap(id -> documentWithId(id, result)) //
Expand Down Expand Up @@ -498,9 +499,9 @@ protected MongoCollection<Document> getCollection(DBRef dbref) {
.getCollection(dbref.getCollectionName(), Document.class);
}

protected MongoCollection<Document> getCollection(ReferenceContext context) {
protected MongoCollection<Document> getCollection(ReferenceCollection context) {

return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -49,7 +43,7 @@ public DefaultReferenceLoader(MongoDatabaseFactory mongoDbFactory) {
}

@Override
public Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext context) {
public Iterable<Document> bulkFetch(DocumentReferenceQuery filter, ReferenceCollection context) {

MongoCollection<Document> collection = getCollection(context);

Expand All @@ -63,9 +57,9 @@ public Stream<Document> bulkFetch(ReferenceFilter filter, ReferenceContext conte
return filter.apply(collection);
}

protected MongoCollection<Document> getCollection(ReferenceContext context) {
protected MongoCollection<Document> getCollection(ReferenceCollection context) {

return MongoDatabaseUtils.getDatabase(context.database, mongoDbFactory).getCollection(context.collection,
return MongoDatabaseUtils.getDatabase(context.getDatabase(), mongoDbFactory).getCollection(context.getCollection(),
Document.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,24 +38,26 @@ public ReferenceLoader getReferenceLoader() {
@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceReader referenceReader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {

if (isLazyReference(property)) {
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction);
return createLazyLoadingProxy(property, source, referenceReader, lookupFunction, resultConversionFunction);
}

return referenceReader.readReference(property, source, lookupFunction);
return referenceReader.readReference(property, source, lookupFunction, resultConversionFunction);
}

private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceReader referenceReader, BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction);
ReferenceReader referenceReader, LookupFunction lookupFunction,
ResultConversionFunction resultConversionFunction) {
return new LazyLoadingProxyGenerator(referenceReader).createLazyLoadingProxy(property, source, lookupFunction,
resultConversionFunction);
}

protected boolean isLazyReference(MongoPersistentProperty property) {

if (property.findAnnotation(DocumentReference.class) != null) {
return property.findAnnotation(DocumentReference.class).lazy();
if (property.isDocumentReference()) {
return property.getDocumentReference().lazy();
}

return property.getDBRef() != null && property.getDBRef().lazy();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.convert;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;

/**
* @author Christoph Strobl
* @since 3.3
*/
class DocumentPointerFactory {

private ConversionService conversionService;
private MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private Map<String, LinkageDocument> linkageMap;

public DocumentPointerFactory(ConversionService conversionService,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {

this.conversionService = conversionService;
this.mappingContext = mappingContext;
this.linkageMap = new HashMap<>();
}

public DocumentPointer<?> computePointer(MongoPersistentProperty property, Object value, Class<?> typeHint) {

if (value instanceof LazyLoadingProxy) {
return () -> ((LazyLoadingProxy) value).getSource();
}

if (conversionService.canConvert(typeHint, DocumentPointer.class)) {
return conversionService.convert(value, DocumentPointer.class);
} else {

MongoPersistentEntity<?> persistentEntity = mappingContext
.getPersistentEntity(property.getAssociationTargetType());

if (!property.getDocumentReference().lookup().toLowerCase().replaceAll("\\s", "").replaceAll("'", "")
.equals("{_id:?#{#target}}")) {

return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), key -> {
return new LinkageDocument(key);
}).get(persistentEntity,
BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value));
}

// just take the id as a reference
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
}
}

static class LinkageDocument {

String lookup;
org.bson.Document fetchDocument;
Map<Integer, String> mapMap;

public LinkageDocument(String lookup) {

this.lookup = lookup;
String targetLookup = lookup;

Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}");

Matcher matcher = pattern.matcher(lookup);
int index = 0;
mapMap = new LinkedHashMap<>();
while (matcher.find()) {

String expr = matcher.group();
mapMap.put(Integer.valueOf(index), expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "")
.replace("target.", "").replaceAll("'", ""));
targetLookup = targetLookup.replace(expr, index + "");
index++;
}

fetchDocument = org.bson.Document.parse(targetLookup);
}

org.bson.Document get(MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {

org.bson.Document targetDocument = new Document();

// TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing?
// like we have it ordered by index values and could provide the parameter array from it.

for (Entry<String, Object> entry : fetchDocument.entrySet()) {

if (entry.getKey().equals("target")) {

String refKey = mapMap.get(entry.getValue());

if (persistentEntity.hasIdProperty()) {
targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty()));
} else {
targetDocument.put(refKey, propertyAccessor.getBean());
}
continue;
}

Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey()));
String refKey = mapMap.get(entry.getValue());
targetDocument.put(refKey, target);
}
return targetDocument;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -54,11 +50,12 @@ public LazyLoadingProxyGenerator(ReferenceReader referenceReader) {
this.objenesis = new ObjenesisStd(true);
}

public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
public Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
ResultConversionFunction resultConversionFunction) {

Class<?> propertyType = property.getType();
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction);
LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, source, referenceReader, lookupFunction,
resultConversionFunction);

if (!propertyType.isInterface()) {

Expand Down Expand Up @@ -105,27 +102,30 @@ public static class LazyLoadingInterceptor
private volatile boolean resolved;
private @org.springframework.lang.Nullable Object result;
private Object source;
private BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction;
private LookupFunction lookupFunction;
private ResultConversionFunction resultConversionFunction;

private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD;
private final Method INITIALIZE_METHOD, TO_DBREF_METHOD, FINALIZE_METHOD, GET_SOURCE_METHOD;

{
try {
INITIALIZE_METHOD = LazyLoadingProxy.class.getMethod("getTarget");
TO_DBREF_METHOD = LazyLoadingProxy.class.getMethod("toDBRef");
FINALIZE_METHOD = Object.class.getDeclaredMethod("finalize");
GET_SOURCE_METHOD = LazyLoadingProxy.class.getMethod("getSource");
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public LazyLoadingInterceptor(MongoPersistentProperty property, Object source, ReferenceReader reader,
BiFunction<ReferenceContext, ReferenceFilter, Stream<Document>> lookupFunction) {
LookupFunction lookupFunction, ResultConversionFunction resultConversionFunction) {

this.property = property;
this.source = source;
this.referenceReader = reader;
this.lookupFunction = lookupFunction;
this.resultConversionFunction = resultConversionFunction;
}

@Nullable
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 6ed274b

Please # to comment.