Browse Source

DATAMONGO-348 - Add support for lazy loading of DbRefs.

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.

Introduced a DelegatingDbRefResolver that can delegate to a DbRefResolveCallback in order  to perform the actual DbRef resolution.
We now use cglib-proxies if necessary if the referenced association is a concrete class.

Added unit tests for lazy loading of interface types, concrete collection types and concrete domain types. Exposed state from LazyLoadingInterceptor for better testability.

Added unit tests for lazy loading of classes with custom PersistenceConstructor.
Moved integration tests for PersonRepository into its own test class.
pull/92/head
Thomas Darimont 12 years ago committed by Oliver Gierke
parent
commit
b808fd3003
  1. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java
  2. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java
  4. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolveCallback.java
  5. 33
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java
  6. 215
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  7. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DBRef.java
  8. 20
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  9. 191
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  10. 63
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
  11. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java
  12. 151
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryLazyLoadingIntegrationTests.java
  13. 38
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/User.java

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java

@ -1,6 +1,22 @@
/*
* 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; package org.springframework.data.mongodb;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import com.mongodb.DB; import com.mongodb.DB;
@ -8,6 +24,7 @@ import com.mongodb.DB;
* Interface for factories creating {@link DB} instances. * Interface for factories creating {@link DB} instances.
* *
* @author Mark Pollack * @author Mark Pollack
* @author Thomas Darimont
*/ */
public interface MongoDbFactory { public interface MongoDbFactory {
@ -27,4 +44,11 @@ public interface MongoDbFactory {
* @throws DataAccessException * @throws DataAccessException
*/ */
DB getDb(String dbName) throws DataAccessException; DB getDb(String dbName) throws DataAccessException;
/**
* Exposes a shared {@link MongoExceptionTranslator}.
*
* @return
*/
MongoExceptionTranslator getExceptionTranslator();
} }

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -145,7 +145,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
private final MongoConverter mongoConverter; private final MongoConverter mongoConverter;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext; private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoDbFactory mongoDbFactory; private final MongoDbFactory mongoDbFactory;
private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
private final QueryMapper queryMapper; private final QueryMapper queryMapper;
private final UpdateMapper updateMapper; private final UpdateMapper updateMapper;
@ -1797,7 +1796,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
* @return * @return
*/ */
private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) {
RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); RuntimeException resolved = this.mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex);
return resolved == null ? ex : resolved; return resolved == null ? ex : resolved;
} }

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,6 +34,7 @@ import com.mongodb.WriteConcern;
* *
* @author Mark Pollack * @author Mark Pollack
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
@ -42,6 +43,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
private final boolean mongoInstanceCreated; private final boolean mongoInstanceCreated;
private final UserCredentials credentials; private final UserCredentials credentials;
private WriteConcern writeConcern; private WriteConcern writeConcern;
private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
/** /**
* Create an instance of {@link SimpleMongoDbFactory} given the {@link Mongo} instance and database name. * Create an instance of {@link SimpleMongoDbFactory} given the {@link Mongo} instance and database name.
@ -138,4 +140,12 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
private static String parseChars(char[] chars) { private static String parseChars(char[] chars) {
return chars == null ? null : String.valueOf(chars); return chars == null ? null : String.valueOf(chars);
} }
/* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public MongoExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
} }

32
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolveCallback.java

@ -0,0 +1,32 @@
/*
* Copyright 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.core.convert;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
/**
* Callback interface to be used in conjunction with {@link DbRefResolver}.
*
* @author Thomas Darimont
*/
interface DbRefResolveCallback {
/**
* @param property
* @return
*/
Object resolve(MongoPersistentProperty property);
}

33
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java

@ -0,0 +1,33 @@
/*
* Copyright 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.core.convert;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
/**
* Used to resolve associations annotated with {@link org.springframework.data.mongodb.core.mapping.DBRef}.
*
* @author Thomas Darimont
*/
interface DbRefResolver {
/**
* @param property
* @param callback
* @return
*/
Object resolve(MongoPersistentProperty property, DbRefResolveCallback callback);
}

215
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

@ -24,8 +24,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
@ -33,6 +36,7 @@ import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.Association; import org.springframework.data.mapping.Association;
@ -86,6 +90,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
protected boolean useFieldAccessOnly = true; protected boolean useFieldAccessOnly = true;
protected MongoTypeMapper typeMapper; protected MongoTypeMapper typeMapper;
protected String mapKeyDotReplacement = null; protected String mapKeyDotReplacement = null;
protected DbRefResolver dbRefResolver;
private SpELContext spELContext; private SpELContext spELContext;
@ -110,6 +115,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
this.idMapper = new QueryMapper(this); this.idMapper = new QueryMapper(this);
this.spELContext = new SpELContext(DBObjectPropertyAccessor.INSTANCE); this.spELContext = new SpELContext(DBObjectPropertyAccessor.INSTANCE);
this.dbRefResolver = new DelegatingDbRefResolver();
} }
/** /**
@ -234,7 +240,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
parent); parent);
} }
private <S extends Object> S read(final MongoPersistentEntity<S> entity, final DBObject dbo, Object parent) { private <S extends Object> S read(final MongoPersistentEntity<S> entity, final DBObject dbo, final Object parent) {
final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(dbo, spELContext); final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(dbo, spELContext);
@ -261,12 +267,20 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
// Handle associations // Handle associations
entity.doWithAssociations(new AssociationHandler<MongoPersistentProperty>() { entity.doWithAssociations(new AssociationHandler<MongoPersistentProperty>() {
public void doWithAssociation(Association<MongoPersistentProperty> association) { public void doWithAssociation(Association<MongoPersistentProperty> association) {
MongoPersistentProperty inverseProp = association.getInverse(); MongoPersistentProperty inverseProp = association.getInverse();
Object obj = getValueInternal(inverseProp, dbo, evaluator, result);
wrapper.setProperty(inverseProp, obj); Object obj = dbRefResolver.resolve(inverseProp, new AbstractDbRefResolveCallback(mongoDbFactory) {
@Override
public Object doResolve(MongoPersistentProperty property) {
return getValueInternal(property, dbo, evaluator, parent);
}
});
wrapper.setProperty(inverseProp, obj);
} }
}); });
return result; return result;
@ -793,7 +807,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Object dbObjItem = sourceValue.get(i); Object dbObjItem = sourceValue.get(i);
if (dbObjItem instanceof DBRef) { if (dbObjItem instanceof DBRef) {
items.add(DBRef.class.equals(rawComponentType) ? dbObjItem : read(componentType, ((DBRef) dbObjItem).fetch(), items.add(DBRef.class.equals(rawComponentType) ? dbObjItem : read(componentType, readRef((DBRef) dbObjItem),
parent)); parent));
} else if (dbObjItem instanceof DBObject) { } else if (dbObjItem instanceof DBObject) {
items.add(read(componentType, (DBObject) dbObjItem, parent)); items.add(read(componentType, (DBObject) dbObjItem, parent));
@ -841,7 +855,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (value instanceof DBObject) { if (value instanceof DBObject) {
map.put(key, read(valueType, (DBObject) value, parent)); map.put(key, read(valueType, (DBObject) value, parent));
} else if (value instanceof DBRef) { } else if (value instanceof DBRef) {
map.put(key, DBRef.class.equals(rawValueType) ? value : read(valueType, ((DBRef) value).fetch())); map.put(key, DBRef.class.equals(rawValueType) ? value : read(valueType, readRef((DBRef) value)));
} else { } else {
Class<?> valueClass = valueType == null ? null : valueType.getType(); Class<?> valueClass = valueType == null ? null : valueType.getType();
map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass)); map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));
@ -1051,7 +1065,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
return (T) conversionService.convert(value, rawType); return (T) conversionService.convert(value, rawType);
} else if (value instanceof DBRef) { } else if (value instanceof DBRef) {
return (T) (rawType.equals(DBRef.class) ? value : read(type, ((DBRef) value).fetch(), parent)); return (T) (rawType.equals(DBRef.class) ? value : read(type, readRef((DBRef) value), parent));
} else if (value instanceof BasicDBList) { } else if (value instanceof BasicDBList) {
return (T) readCollectionOrArray(type, (BasicDBList) value, parent); return (T) readCollectionOrArray(type, (BasicDBList) value, parent);
} else if (value instanceof DBObject) { } else if (value instanceof DBObject) {
@ -1061,4 +1075,193 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
} }
/**
* Performs the fetch operation for the given {@link DBRef}.
*
* @param ref
* @return
*/
DBObject readRef(DBRef ref) {
return ref.fetch();
}
/**
* @author Thomas Darimont
*/
static abstract class AbstractDbRefResolveCallback implements DbRefResolveCallback {
private final MongoDbFactory mongoDbFactory;
/**
* @param mongoDbFactory
*/
public AbstractDbRefResolveCallback(MongoDbFactory mongoDbFactory) {
this.mongoDbFactory = mongoDbFactory;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolveCallback#resolve(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty)
*/
@Override
public Object resolve(MongoPersistentProperty property) {
try {
return doResolve(property);
} catch (RuntimeException ex) {
DataAccessException tex = mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex);
throw tex != null ? tex : ex;
}
}
/**
* @param property
* @return
*/
protected abstract Object doResolve(MongoPersistentProperty property);
}
/**
* A marker interface that is used to mark lazy loading proxies.
*
* @author Thomas Darimont
*/
public static interface LazyLoadingProxy {}
/**
* A {@link MethodInterceptor} that is used within a lazy loading proxy. The property resolving is delegated to a
* {@link DbRefResolveCallback}. The resolving process is triggered by a method invocation on the proxy and is
* guaranteed to be performed only once.
*
* @author Thomas Darimont
*/
public static class LazyLoadingInterceptor implements MethodInterceptor {
private DbRefResolveCallback callback;
private MongoPersistentProperty property;
private volatile boolean resolved;
private Object result;
/**
* @param callback
* @param property
*/
public LazyLoadingInterceptor(DbRefResolveCallback callback, MongoPersistentProperty property) {
this.callback = callback;
this.property = property;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (!resolved) {
this.result = resolve();
this.resolved = true;
}
return invocation.getMethod().invoke(result, invocation.getArguments());
}
/**
* @return
*/
private synchronized Object resolve() {
if (!resolved) {
try {
return callback.resolve(property);
} catch (Exception ex) {
throw new RuntimeException("Could not resolve lazy DBRef: " + property, ex);
} finally {
cleanup();
}
}
return result;
}
/**
* Visible for testing.
*
* @return
*/
public boolean isResolved() {
return resolved;
}
/**
* Visible for testing.
*
* @return the result
*/
public Object getResult() {
return result;
}
/**
* Cleans up unnecessary references to avoid memory leaks.
*/
private void cleanup() {
this.callback = null;
this.property = null;
}
}
/**
* A {@link DbRefResolver} that resolves {@link org.springframework.data.mongodb.core.mapping.DBRef}s by delegating to
* a {@link DbRefResolveCallback} than is able to generate lazy loading proxies.
*
* @author Thomas Darimont
*/
static class DelegatingDbRefResolver implements DbRefResolver {
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolve(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolveCallback)
*/
public Object resolve(MongoPersistentProperty property, DbRefResolveCallback callback) {
Assert.notNull(property, "property must not be null!");
Assert.notNull(callback, "callback must not be null!");
if (isLazyDbRef(property)) {
return createLazyLoadingProxy(property, callback);
}
return callback.resolve(property);
}
/**
* @param property
* @param callback
* @return
*/
public Object createLazyLoadingProxy(MongoPersistentProperty property, DbRefResolveCallback callback) {
ProxyFactory proxyFactory = new ProxyFactory();
if (property.getRawType().isInterface()) {
proxyFactory.addInterface(property.getRawType());
} else {
proxyFactory.setProxyTargetClass(true);
proxyFactory.setTargetClass(property.getRawType());
}
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addAdvice(new LazyLoadingInterceptor(callback, property));
return proxyFactory.getProxy();
}
/**
* @param property
* @return
*/
protected boolean isLazyDbRef(MongoPersistentProperty property) {
return property.getDBRef() != null && property.getDBRef().lazy();
}
}
} }

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DBRef.java

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,7 +27,8 @@ import org.springframework.data.annotation.Reference;
* An annotation that indicates the annotated field is to be stored using a {@link com.mongodb.DBRef}. * An annotation that indicates the annotated field is to be stored using a {@link com.mongodb.DBRef}.
* *
* @author Jon Brisbin * @author Jon Brisbin
* @authot Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -41,4 +42,11 @@ public @interface DBRef {
* @return * @return
*/ */
String db() default ""; String db() default "";
/**
* Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}.
*
* @return
*/
boolean lazy() default false;
} }

20
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

@ -64,15 +64,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
MongoTemplate template; MongoTemplate template;
@Mock @Mock MongoDbFactory factory;
MongoDbFactory factory; @Mock Mongo mongo;
@Mock @Mock DB db;
Mongo mongo; @Mock DBCollection collection;
@Mock
DB db;
@Mock
DBCollection collection;
MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter; MappingMongoConverter converter;
MongoMappingContext mappingContext; MongoMappingContext mappingContext;
@ -84,6 +81,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
this.template = new MongoTemplate(factory, converter); this.template = new MongoTemplate(factory, converter);
when(factory.getDb()).thenReturn(db); when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(Mockito.any(String.class))).thenReturn(collection); when(db.getCollection(Mockito.any(String.class))).thenReturn(collection);
} }
@ -228,14 +226,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
class AutogenerateableId { class AutogenerateableId {
@Id @Id BigInteger id;
BigInteger id;
} }
class NotAutogenerateableId { class NotAutogenerateableId {
@Id @Id Integer id;
Integer id;
public Pattern getId() { public Pattern getId() {
return Pattern.compile("."); return Pattern.compile(".");

191
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -29,6 +30,7 @@ import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -43,6 +45,8 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@ -54,8 +58,11 @@ import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.MappingInstantiationException; import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.DBObjectTestUtils; import org.springframework.data.mongodb.core.DBObjectTestUtils;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.NestedType;
import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.ProjectingType;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.LazyLoadingInterceptor;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.LazyLoadingProxy;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@ -87,6 +94,8 @@ public class MappingMongoConverterUnitTests {
@Before @Before
public void setUp() { public void setUp() {
when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());
mappingContext = new MongoMappingContext(); mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(context); mappingContext.setApplicationContext(context);
mappingContext.afterPropertiesSet(); mappingContext.afterPropertiesSet();
@ -1451,6 +1460,120 @@ public class MappingMongoConverterUnitTests {
assertThat(aValue.get("c"), is((Object) "C")); assertThat(aValue.get("c"), is((Object) "C"));
} }
/**
* @see DATAMONGO-348
*/
@Test
public void lazyLoadingProxyForLazyDbRefOnInterface() {
String id = "42";
String value = "bubu";
MappingMongoConverter converterSpy = spy(converter);
doReturn(new BasicDBObject("_id", id).append("value", value)).when(converterSpy).readRef((DBRef) any());
BasicDBObject dbo = new BasicDBObject();
ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
lazyDbRefs.dbRefToInterface = new LinkedList<LazyDbRefTarget>(Arrays.asList(new LazyDbRefTarget("1")));
converterSpy.write(lazyDbRefs, dbo);
ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, dbo);
assertTrue(result.dbRefToInterface instanceof LazyLoadingProxy);
LazyLoadingInterceptor interceptor = extractInterceptor(result.dbRefToInterface);
assertThat(interceptor.isResolved(), is(false));
assertThat(result.dbRefToInterface.get(0).getId(), is(id));
assertThat(interceptor.isResolved(), is(true));
assertThat(result.dbRefToInterface.get(0).getValue(), is(value));
}
/**
* @see DATAMONGO-348
*/
@Test
public void lazyLoadingProxyForLazyDbRefOnConcreteCollection() {
String id = "42";
String value = "bubu";
MappingMongoConverter converterSpy = spy(converter);
doReturn(new BasicDBObject("_id", id).append("value", value)).when(converterSpy).readRef((DBRef) any());
BasicDBObject dbo = new BasicDBObject();
ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
lazyDbRefs.dbRefToConcreteCollection = new ArrayList<LazyDbRefTarget>(Arrays.asList(new LazyDbRefTarget(id, value)));
converterSpy.write(lazyDbRefs, dbo);
ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, dbo);
assertTrue(result.dbRefToConcreteCollection instanceof LazyLoadingProxy);
LazyLoadingInterceptor interceptor = extractInterceptor(result.dbRefToConcreteCollection);
assertThat(interceptor.isResolved(), is(false));
assertThat(result.dbRefToConcreteCollection.get(0).getId(), is(id));
assertThat(interceptor.isResolved(), is(true));
assertThat(result.dbRefToConcreteCollection.get(0).getValue(), is(value));
}
/**
* @see DATAMONGO-348
*/
@Test
public void lazyLoadingProxyForLazyDbRefOnConcreteType() {
String id = "42";
String value = "bubu";
MappingMongoConverter converterSpy = spy(converter);
doReturn(new BasicDBObject("_id", id).append("value", value)).when(converterSpy).readRef((DBRef) any());
BasicDBObject dbo = new BasicDBObject();
ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
lazyDbRefs.dbRefToConcreteType = new LazyDbRefTarget(id, value);
converterSpy.write(lazyDbRefs, dbo);
ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, dbo);
assertTrue(result.dbRefToConcreteType instanceof LazyLoadingProxy);
LazyLoadingInterceptor interceptor = extractInterceptor(result.dbRefToConcreteType);
assertThat(interceptor.isResolved(), is(false));
assertThat(result.dbRefToConcreteType.getId(), is(id));
assertThat(interceptor.isResolved(), is(true));
assertThat(result.dbRefToConcreteType.getValue(), is(value));
}
/**
* @see DATAMONGO-348
*/
@Test
public void lazyLoadingProxyForLazyDbRefOnConcreteTypeWithPersistenceConstructor() {
String id = "42";
String value = "bubu";
MappingMongoConverter converterSpy = spy(converter);
doReturn(new BasicDBObject("_id", id).append("value", value)).when(converterSpy).readRef((DBRef) any());
BasicDBObject dbo = new BasicDBObject();
ClassWithLazyDbRefs lazyDbRefs = new ClassWithLazyDbRefs();
lazyDbRefs.dbRefToConcreteTypeWithPersistenceConstructor = new LazyDbRefTargetWithPeristenceConstructor(
(Object) id, (Object) value);
converterSpy.write(lazyDbRefs, dbo);
ClassWithLazyDbRefs result = converterSpy.read(ClassWithLazyDbRefs.class, dbo);
assertTrue(result.dbRefToConcreteTypeWithPersistenceConstructor instanceof LazyLoadingProxy);
LazyLoadingInterceptor interceptor = extractInterceptor(result.dbRefToConcreteTypeWithPersistenceConstructor);
assertThat(interceptor.isResolved(), is(false));
assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor.getId(), is(id));
assertThat(interceptor.isResolved(), is(true));
assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor.getValue(), is(value));
assertThat(result.dbRefToConcreteTypeWithPersistenceConstructor.isPersistanceConstructorCalled(), is(true));
}
private LazyLoadingInterceptor extractInterceptor(Object proxy) {
return (LazyLoadingInterceptor) ((Advisor) ((Advised) proxy).getAdvisors()[0]).getAdvice();
}
@Document @Document
class MapDBRef { class MapDBRef {
@org.springframework.data.mongodb.core.mapping.DBRef Map<String, MapDBRefVal> map; @org.springframework.data.mongodb.core.mapping.DBRef Map<String, MapDBRefVal> map;
@ -1678,4 +1801,72 @@ public class MappingMongoConverterUnitTests {
return m_property; return m_property;
} }
} }
static class ClassWithLazyDbRefs {
@Id String id;
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) List<LazyDbRefTarget> dbRefToInterface;
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) ArrayList<LazyDbRefTarget> dbRefToConcreteCollection;
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) LazyDbRefTarget dbRefToConcreteType;
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) LazyDbRefTargetWithPeristenceConstructor dbRefToConcreteTypeWithPersistenceConstructor;
}
static class LazyDbRefTarget {
@Id String id;
String value;
public LazyDbRefTarget() {
this(null);
}
public LazyDbRefTarget(String id) {
this(id, null);
}
public LazyDbRefTarget(String id, String value) {
this.id = id;
this.value = value;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
static class LazyDbRefTargetWithPeristenceConstructor extends LazyDbRefTarget {
boolean persistanceConstructorCalled;
public LazyDbRefTargetWithPeristenceConstructor() {}
@PersistenceConstructor
public LazyDbRefTargetWithPeristenceConstructor(String id, String value) {
super(id, value);
this.persistanceConstructorCalled = true;
}
public LazyDbRefTargetWithPeristenceConstructor(Object id, Object value) {
super(id.toString(), value.toString());
}
public boolean isPersistanceConstructorCalled() {
return persistanceConstructorCalled;
}
}
} }

63
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java

@ -15,7 +15,9 @@
*/ */
package org.springframework.data.mongodb.repository; package org.springframework.data.mongodb.repository;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.core.geo.Point;
@ -28,6 +30,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
* Sample domain class. * Sample domain class.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
@Document @Document
public class Person extends Contact { public class Person extends Contact {
@ -38,21 +41,23 @@ public class Person extends Contact {
private String firstname; private String firstname;
private String lastname; private String lastname;
@Indexed(unique = true, dropDups = true) @Indexed(unique = true, dropDups = true) private String email;
private String email;
private Integer age; private Integer age;
@SuppressWarnings("unused") @SuppressWarnings("unused") private Sex sex;
private Sex sex;
Date createdAt; Date createdAt;
@GeoSpatialIndexed @GeoSpatialIndexed private Point location;
private Point location;
private Address address; private Address address;
private Set<Address> shippingAddresses; private Set<Address> shippingAddresses;
@DBRef @DBRef User creator;
User creator;
@DBRef(lazy = true) User coworker;
@DBRef(lazy = true) List<User> fans;
@DBRef(lazy = true) ArrayList<User> realFans;
Credentials credentials; Credentials credentials;
@ -193,6 +198,48 @@ public class Person extends Contact {
return String.format("%s %s", firstname, lastname); 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;
}
/**
* @return the realFans
*/
public ArrayList<User> getRealFans() {
return realFans;
}
/**
* @param realFans the realFans to set
*/
public void setRealFans(ArrayList<User> realFans) {
this.realFans = realFans;
}
/**
* @return the coworker
*/
public User getCoworker() {
return coworker;
}
/**
* @param coworker the coworker to set
*/
public void setCoworker(User coworker) {
this.coworker = coworker;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* *

6
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2012 the original author or authors. * Copyright 2010-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import org.springframework.test.context.ContextConfiguration;
* Integration test for {@link PersonRepository}. * Integration test for {@link PersonRepository}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
@ContextConfiguration @ContextConfiguration
public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {}
}

151
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryLazyLoadingIntegrationTests.java

@ -0,0 +1,151 @@
/*
* Copyright 2010-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.repository;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.LazyLoadingInterceptor;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Integration test for {@link PersonRepository} for lazy loading support.
*
* @author Thomas Darimont
*/
@ContextConfiguration(locations = "PersonRepositoryIntegrationTests-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class PersonRepositoryLazyLoadingIntegrationTests {
@Autowired PersonRepository repository;
@Autowired MongoOperations operations;
@Before
public void setUp() throws InterruptedException {
repository.deleteAll();
operations.remove(new org.springframework.data.mongodb.core.query.Query(), User.class);
}
/**
* @see DATAMONGO-348
*/
@Test
public void shouldLoadAssociationWithDbRefOnInterfaceAndLazyLoadingEnabled() throws Exception {
User thomas = new User();
thomas.username = "Thomas";
operations.save(thomas);
Person person = new Person();
person.setFirstname("Oliver");
person.setFans(Arrays.asList(thomas));
person.setRealFans(new ArrayList<User>(Arrays.asList(thomas)));
repository.save(person);
Person oliver = repository.findOne(person.id);
List<User> fans = oliver.getFans();
LazyLoadingInterceptor interceptor = extractInterceptor(fans);
assertThat(interceptor.getResult(), is(nullValue()));
assertThat(interceptor.isResolved(), is(false));
User user = fans.get(0);
assertThat(interceptor.getResult(), is(notNullValue()));
assertThat(interceptor.isResolved(), is(true));
assertThat(user.getUsername(), is(thomas.getUsername()));
}
/**
* @see DATAMONGO-348
*/
@Test
public void shouldLoadAssociationWithDbRefOnConcreteCollectionAndLazyLoadingEnabled() throws Exception {
User thomas = new User();
thomas.username = "Thomas";
operations.save(thomas);
Person person = new Person();
person.setFirstname("Oliver");
person.setFans(Arrays.asList(thomas));
person.setRealFans(new ArrayList<User>(Arrays.asList(thomas)));
repository.save(person);
Person oliver = repository.findOne(person.id);
List<User> realFans = oliver.getRealFans();
LazyLoadingInterceptor interceptor = extractInterceptor(realFans);
assertThat(interceptor.getResult(), is(nullValue()));
assertThat(interceptor.isResolved(), is(false));
User realFan = realFans.get(0);
assertThat(interceptor.getResult(), is(notNullValue()));
assertThat(interceptor.isResolved(), is(true));
assertThat(realFan.getUsername(), is(thomas.getUsername()));
realFans = oliver.getRealFans();
assertThat(interceptor.getResult(), is(notNullValue()));
assertThat(interceptor.isResolved(), is(true));
realFan = realFans.get(0);
assertThat(realFan.getUsername(), is(thomas.getUsername()));
}
/**
* @see DATAMONGO-348
*/
@Test
public void shouldLoadAssociationWithDbRefOnConcreteDomainClassAndLazyLoadingEnabled() throws Exception {
User thomas = new User();
thomas.username = "Thomas";
operations.save(thomas);
Person person = new Person();
person.setFirstname("Oliver");
person.setCoworker(thomas);
repository.save(person);
Person oliver = repository.findOne(person.id);
User coworker = oliver.getCoworker();
LazyLoadingInterceptor interceptor = extractInterceptor(coworker);
assertThat(interceptor.getResult(), is(nullValue()));
assertThat(interceptor.isResolved(), is(false));
assertThat(coworker.getUsername(), is(thomas.getUsername()));
assertThat(interceptor.isResolved(), is(true));
assertThat(coworker.getUsername(), is(thomas.getUsername()));
}
private LazyLoadingInterceptor extractInterceptor(Object proxy) {
return (LazyLoadingInterceptor) ((Advisor) ((Advised) proxy).getAdvisors()[0]).getAdvice();
}
}

38
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/User.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2012 the original author or authors. * Copyright 2012-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,10 +18,42 @@ package org.springframework.data.mongodb.repository;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Document @Document
public class User { public class User {
@Id @Id String id;
String id;
String username; String username;
/**
* @return the id
*/
public String getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
} }

Loading…
Cancel
Save