From b808fd30035547e46f7f294e5012e5769dead18e Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 15 Oct 2013 18:58:06 +0200 Subject: [PATCH] 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. --- .../data/mongodb/MongoDbFactory.java | 24 ++ .../data/mongodb/core/MongoTemplate.java | 3 +- .../mongodb/core/SimpleMongoDbFactory.java | 12 +- .../core/convert/DbRefResolveCallback.java | 32 +++ .../mongodb/core/convert/DbRefResolver.java | 33 +++ .../core/convert/MappingMongoConverter.java | 215 +++++++++++++++++- .../data/mongodb/core/mapping/DBRef.java | 12 +- .../mongodb/core/MongoTemplateUnitTests.java | 20 +- .../MappingMongoConverterUnitTests.java | 191 ++++++++++++++++ .../data/mongodb/repository/Person.java | 63 ++++- .../PersonRepositoryIntegrationTests.java | 6 +- ...RepositoryLazyLoadingIntegrationTests.java | 151 ++++++++++++ .../data/mongodb/repository/User.java | 38 +++- 13 files changed, 763 insertions(+), 37 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolveCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryLazyLoadingIntegrationTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java index 0276928a1..687fc6943 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java +++ b/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; import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.MongoExceptionTranslator; import com.mongodb.DB; @@ -8,6 +24,7 @@ import com.mongodb.DB; * Interface for factories creating {@link DB} instances. * * @author Mark Pollack + * @author Thomas Darimont */ public interface MongoDbFactory { @@ -27,4 +44,11 @@ public interface MongoDbFactory { * @throws DataAccessException */ DB getDb(String dbName) throws DataAccessException; + + /** + * Exposes a shared {@link MongoExceptionTranslator}. + * + * @return + */ + MongoExceptionTranslator getExceptionTranslator(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index beab06007..330c63ea7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/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 MappingContext, MongoPersistentProperty> mappingContext; private final MongoDbFactory mongoDbFactory; - private final MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); private final QueryMapper queryMapper; private final UpdateMapper updateMapper; @@ -1797,7 +1796,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { * @return */ private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { - RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException resolved = this.mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex); return resolved == null ? ex : resolved; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java index e5a1a46a9..658c34ccd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import com.mongodb.WriteConcern; * * @author Mark Pollack * @author Oliver Gierke + * @author Thomas Darimont */ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { @@ -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. @@ -138,4 +140,12 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { 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; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolveCallback.java new file mode 100644 index 000000000..5326610ca --- /dev/null +++ b/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); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java new file mode 100644 index 000000000..7d84a3254 --- /dev/null +++ b/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); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index a0911efce..81b70d8e3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -24,8 +24,11 @@ import java.util.List; 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; @@ -33,6 +36,7 @@ import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.dao.DataAccessException; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.TypeMapper; import org.springframework.data.mapping.Association; @@ -86,6 +90,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App protected boolean useFieldAccessOnly = true; protected MongoTypeMapper typeMapper; protected String mapKeyDotReplacement = null; + protected DbRefResolver dbRefResolver; private SpELContext spELContext; @@ -110,6 +115,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this.idMapper = new QueryMapper(this); this.spELContext = new SpELContext(DBObjectPropertyAccessor.INSTANCE); + this.dbRefResolver = new DelegatingDbRefResolver(); } /** @@ -234,7 +240,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App parent); } - private S read(final MongoPersistentEntity entity, final DBObject dbo, Object parent) { + private S read(final MongoPersistentEntity entity, final DBObject dbo, final Object parent) { final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(dbo, spELContext); @@ -261,12 +267,20 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App // Handle associations entity.doWithAssociations(new AssociationHandler() { public void doWithAssociation(Association association) { + 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; @@ -793,7 +807,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Object dbObjItem = sourceValue.get(i); 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)); } else if (dbObjItem instanceof DBObject) { items.add(read(componentType, (DBObject) dbObjItem, parent)); @@ -841,7 +855,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (value instanceof DBObject) { map.put(key, read(valueType, (DBObject) value, parent)); } 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 { Class valueClass = valueType == null ? null : valueType.getType(); map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass)); @@ -1051,7 +1065,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { return (T) conversionService.convert(value, rawType); } 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) { return (T) readCollectionOrArray(type, (BasicDBList) value, parent); } 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(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DBRef.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DBRef.java index a5b0267ba..3272ae41f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/DBRef.java +++ b/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"); * 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}. * * @author Jon Brisbin - * @authot Oliver Gierke + * @author Oliver Gierke + * @author Thomas Darimont */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -41,4 +42,11 @@ public @interface DBRef { * @return */ String db() default ""; + + /** + * Controls whether the referenced entity should be loaded lazily. This defaults to {@literal false}. + * + * @return + */ + boolean lazy() default false; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index ae08ed5ea..0ae20f548 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -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; @@ -84,6 +81,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { 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); } @@ -228,14 +226,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { class AutogenerateableId { - @Id - BigInteger id; + @Id BigInteger id; } class NotAutogenerateableId { - @Id - Integer id; + @Id Integer id; public Pattern getId() { return Pattern.compile("."); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index f10f81eda..7719c7b04 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import java.math.BigDecimal; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -43,6 +45,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; 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.context.ApplicationContext; 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.mongodb.MongoDbFactory; 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.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.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -87,6 +94,8 @@ public class MappingMongoConverterUnitTests { @Before public void setUp() { + when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator()); + mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); mappingContext.afterPropertiesSet(); @@ -1451,6 +1460,120 @@ public class MappingMongoConverterUnitTests { 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(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(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 class MapDBRef { @org.springframework.data.mongodb.core.mapping.DBRef Map map; @@ -1678,4 +1801,72 @@ public class MappingMongoConverterUnitTests { return m_property; } } + + static class ClassWithLazyDbRefs { + @Id String id; + + @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) List dbRefToInterface; + + @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) ArrayList 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; + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 2a86e1c4f..799720f30 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -15,7 +15,9 @@ */ package org.springframework.data.mongodb.repository; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Set; import org.springframework.data.mongodb.core.geo.Point; @@ -28,6 +30,7 @@ import org.springframework.data.mongodb.core.mapping.Document; * Sample domain class. * * @author Oliver Gierke + * @author Thomas Darimont */ @Document public class Person extends Contact { @@ -38,21 +41,23 @@ public class Person extends Contact { 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
shippingAddresses; - @DBRef - User creator; + @DBRef User creator; + + @DBRef(lazy = true) User coworker; + + @DBRef(lazy = true) List fans; + + @DBRef(lazy = true) ArrayList realFans; Credentials credentials; @@ -193,6 +198,48 @@ public class Person extends Contact { return String.format("%s %s", firstname, lastname); } + /** + * @return the fans + */ + public List getFans() { + return fans; + } + + /** + * @param fans the fans to set + */ + public void setFans(List fans) { + this.fans = fans; + } + + /** + * @return the realFans + */ + public ArrayList getRealFans() { + return realFans; + } + + /** + * @param realFans the realFans to set + */ + public void setRealFans(ArrayList 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) * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java index 51196811e..8dfde08b7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java +++ b/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"); * 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}. * * @author Oliver Gierke + * @author Thomas Darimont */ @ContextConfiguration -public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { -} +public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryLazyLoadingIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryLazyLoadingIntegrationTests.java new file mode 100644 index 000000000..1d2d1a005 --- /dev/null +++ b/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(Arrays.asList(thomas))); + repository.save(person); + + Person oliver = repository.findOne(person.id); + List 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(Arrays.asList(thomas))); + repository.save(person); + + Person oliver = repository.findOne(person.id); + List 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(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/User.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/User.java index a0ae8aa39..2f6b4b1f6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/User.java +++ b/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"); * 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.mongodb.core.mapping.Document; +/** + * @author Oliver Gierke + * @author Thomas Darimont + */ @Document public class User { - @Id - String id; + @Id String id; 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; + } + }