Skip to content

Commit

Permalink
DATAMONGO-348 - Add support for lazy loading of DbRefs.
Browse files Browse the repository at this point in the history
Introduced DbRefResolver interface in order to be able to abstract how a DbRef is resolved that is used in MappingMongoConverter#doWithAssociations. The present behaviour was to resolve a DbRef eagerly. This functionality is now implemented by EagerDbRefResolver. In order to support lazy loading we have to provide some means to define the desired loading behaviour. This can now be done via the "lazy"-Attribute on @DBREF which defaults to false.
If the attribute is set to true the LazyDbRefResolver is used to create a Proxy that eagerly loads the required data on demand when one of the (non-Object) proxy methods is called. MongoDbFactory now exposes a MongoExceptionTranslator that is now used by the MappingMongoConverter and MongoTemplate.
  • Loading branch information
Thomas Darimont committed Oct 28, 2013
1 parent 94d4fa6 commit 1d299c3
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 27 deletions.
@@ -1,13 +1,30 @@
/*
* Copyright 2011-2013 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
*
* http://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;

import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;

import com.mongodb.DB;

/**
* Interface for factories creating {@link DB} instances.
*
* @author Mark Pollack
* @author Thomas Darimont
*/
public interface MongoDbFactory {

Expand All @@ -27,4 +44,11 @@ public interface MongoDbFactory {
* @throws DataAccessException
*/
DB getDb(String dbName) throws DataAccessException;

/**
* Exposes a shared {@link MongoExceptionTranslator}.
*
* @return
*/
MongoExceptionTranslator getExceptionTranslator();
}
Expand Up @@ -145,7 +145,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
private final MongoConverter mongoConverter;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoDbFactory mongoDbFactory;
private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;

Expand Down Expand Up @@ -1797,7 +1796,7 @@ protected void handleAnyWriteResultErrors(WriteResult writeResult, DBObject quer
* @return
*/
private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) {
RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex);
RuntimeException resolved = this.mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex);
return resolved == null ? ex : resolved;
}

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2012 the original author or authors.
* Copyright 2011-2013 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.
Expand Down Expand Up @@ -34,6 +34,7 @@
*
* @author Mark Pollack
* @author Oliver Gierke
* @author Thomas Darimont
*/
public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {

Expand All @@ -42,6 +43,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
private final boolean mongoInstanceCreated;
private final UserCredentials credentials;
private WriteConcern writeConcern;
private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();

/**
* Create an instance of {@link SimpleMongoDbFactory} given the {@link Mongo} instance and database name.
Expand Down Expand Up @@ -138,4 +140,12 @@ public void destroy() throws Exception {
private static String parseChars(char[] chars) {
return chars == null ? null : String.valueOf(chars);
}

/* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public MongoExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
}
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.convert;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -24,8 +25,11 @@
import java.util.Map;
import java.util.Map.Entry;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
Expand Down Expand Up @@ -57,6 +61,8 @@
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.StringUtils;

import com.mongodb.BasicDBList;
Expand Down Expand Up @@ -261,17 +267,29 @@ public void doWithPersistentProperty(MongoPersistentProperty prop) {
// Handle associations
entity.doWithAssociations(new AssociationHandler<MongoPersistentProperty>() {
public void doWithAssociation(Association<MongoPersistentProperty> association) {

MongoPersistentProperty inverseProp = association.getInverse();
Object obj = getValueInternal(inverseProp, dbo, evaluator, result);

wrapper.setProperty(inverseProp, obj);
Object obj = selectDbRefResolverFor(inverseProp).resolve(inverseProp, dbo, evaluator, result);

wrapper.setProperty(inverseProp, obj);
}

});

return result;
}

/**
* @param property
* @return
*/
private DbRefResolver selectDbRefResolverFor(MongoPersistentProperty property) {

return property.getDBRef() != null && property.getDBRef().lazy() ? new LazyDbRefResolver()
: new EagerDbRefResolver();
}

/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.MongoWriter#toDBRef(java.lang.Object, org.springframework.data.mongodb.core.mapping.MongoPersistentProperty)
Expand Down Expand Up @@ -1061,4 +1079,96 @@ private <T> T readValue(Object value, TypeInformation<?> type, Object parent) {
}
}

/**
* Used to resolve associations annotated with {@link org.springframework.data.mongodb.core.mapping.DBRef}.
*
* @author Thomas Darimont
*/
interface DbRefResolver {
Object resolve(MongoPersistentProperty prop, DBObject dbo, final SpELExpressionEvaluator eval, Object parent);
}

/**
* A {@link DbRefResolver} that resolves {@link org.springframework.data.mongodb.core.mapping.DBRef}s eagerly.
*
* @author Thomas Darimont
*/
class EagerDbRefResolver implements DbRefResolver {
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DBRefResolver#resolve()
*/
@Override
public Object resolve(MongoPersistentProperty prop, DBObject dbo, final SpELExpressionEvaluator eval, Object parent) {
return getValueInternal(prop, dbo, eval, parent);
}
}

/**
* A {@link DbRefResolver} that resolves {@link org.springframework.data.mongodb.core.mapping.DBRef}s lazily.
*
* @author Thomas Darimont
*/
class LazyDbRefResolver extends EagerDbRefResolver {

/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DBRefResolver#resolve()
*/
@Override
public Object resolve(final MongoPersistentProperty prop, final DBObject dbo, final SpELExpressionEvaluator eval,
final Object parent) {

class LazyLoadingInterceptor implements MethodInterceptor {

volatile boolean initialized;

Object result;

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {

if (!initialized) {
initialize();
}

return invocation.getMethod().invoke(result, invocation.getArguments());
}

private synchronized void initialize() {

if (!initialized) {
try {
this.result = LazyDbRefResolver.super.resolve(prop, dbo, eval, parent);
this.initialized = true;
} catch (RuntimeException ex) {
throw mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex);
}
cleanupUnnecessaryReferences();
}
}

/**
* Cleans up unnecessary references to avoid memory leaks.
*/
private void cleanupUnnecessaryReferences() {

ReflectionUtils.doWithFields(getClass(), new FieldCallback() {

@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
if (field.getName().startsWith("val$") || field.getName().startsWith("this$1")) {
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, LazyLoadingInterceptor.this, null);
}
}
});
}
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setInterfaces(new Class[] { prop.getRawType() });
proxyFactory.addAdvice(new LazyLoadingInterceptor());

return proxyFactory.getProxy();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2012 by the original author(s).
* Copyright 2011-2013 by the original author(s).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,7 +27,8 @@
* An annotation that indicates the annotated field is to be stored using a {@link com.mongodb.DBRef}.
*
* @author Jon Brisbin
* @authot Oliver Gierke
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -41,4 +42,11 @@
* @return
*/
String db() default "";

/**
* Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}.
*
* @return
*/
boolean lazy() default false;
}
Expand Up @@ -64,15 +64,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {

MongoTemplate template;

@Mock
MongoDbFactory factory;
@Mock
Mongo mongo;
@Mock
DB db;
@Mock
DBCollection collection;
@Mock MongoDbFactory factory;
@Mock Mongo mongo;
@Mock DB db;
@Mock DBCollection collection;

MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter;
MongoMappingContext mappingContext;

Expand All @@ -84,6 +81,7 @@ public void setUp() {
this.template = new MongoTemplate(factory, converter);

when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(Mockito.any(String.class))).thenReturn(collection);
}

Expand Down Expand Up @@ -228,14 +226,12 @@ public void registersDefaultEntityIndexCreatorIfApplicationContextHasOneForDiffe

class AutogenerateableId {

@Id
BigInteger id;
@Id BigInteger id;
}

class NotAutogenerateableId {

@Id
Integer id;
@Id Integer id;

public Pattern getId() {
return Pattern.compile(".");
Expand Down
Expand Up @@ -16,6 +16,7 @@
package org.springframework.data.mongodb.repository;

import java.util.Date;
import java.util.List;
import java.util.Set;

import org.springframework.data.mongodb.core.geo.Point;
Expand All @@ -28,6 +29,7 @@
* Sample domain class.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Document
public class Person extends Contact {
Expand All @@ -38,21 +40,19 @@ public enum Sex {

private String firstname;
private String lastname;
@Indexed(unique = true, dropDups = true)
private String email;
@Indexed(unique = true, dropDups = true) private String email;
private Integer age;
@SuppressWarnings("unused")
private Sex sex;
@SuppressWarnings("unused") private Sex sex;
Date createdAt;

@GeoSpatialIndexed
private Point location;
@GeoSpatialIndexed private Point location;

private Address address;
private Set<Address> shippingAddresses;

@DBRef
User creator;
@DBRef User creator;

@DBRef(lazy = true) List<User> fans;

Credentials credentials;

Expand Down Expand Up @@ -193,6 +193,20 @@ public String getName() {
return String.format("%s %s", firstname, lastname);
}

/**
* @return the fans
*/
public List<User> getFans() {
return fans;
}

/**
* @param fans the fans to set
*/
public void setFans(List<User> fans) {
this.fans = fans;
}

/*
* (non-Javadoc)
*
Expand Down

0 comments on commit 1d299c3

Please sign in to comment.