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; + } + }