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 0276928a10..687fc6943d 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 @@ * 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 beab060076..330c63ea79 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 @@ protected void handleAnyWriteResultErrors(WriteResult writeResult, DBObject quer * @return */ private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { - RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException resolved = this.mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex); return resolved == null ? ex : resolved; } 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 e5a1a46a95..12d2d2e7db 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 @@ * * @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 void destroy() throws Exception { private static String parseChars(char[] chars) { return chars == null ? null : String.valueOf(chars); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator() + */ + @Override + public MongoExceptionTranslator getExceptionTranslator() { + return this.exceptionTranslator; + } } 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 a0911efcec..5d24b27e9d 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,8 +25,11 @@ import java.util.Map; import java.util.Map.Entry; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -57,6 +61,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; import org.springframework.util.StringUtils; import com.mongodb.BasicDBList; @@ -261,17 +267,29 @@ public void doWithPersistentProperty(MongoPersistentProperty prop) { // 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 = selectDbRefResolverFor(inverseProp).resolve(inverseProp, dbo, evaluator, result); + wrapper.setProperty(inverseProp, obj); } + }); return result; } + /** + * @param property + * @return + */ + private DbRefResolver selectDbRefResolverFor(MongoPersistentProperty property) { + + return property.getDBRef() != null && property.getDBRef().lazy() ? new LazyDbRefResolver() + : new EagerDbRefResolver(); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.MongoWriter#toDBRef(java.lang.Object, org.springframework.data.mongodb.core.mapping.MongoPersistentProperty) @@ -1061,4 +1079,96 @@ private T readValue(Object value, TypeInformation type, Object parent) { } } + /** + * Used to resolve associations annotated with {@link org.springframework.data.mongodb.core.mapping.DBRef}. + * + * @author Thomas Darimont + */ + interface DbRefResolver { + Object resolve(MongoPersistentProperty prop, DBObject dbo, final SpELExpressionEvaluator eval, Object parent); + } + + /** + * A {@link DbRefResolver} that resolves {@link org.springframework.data.mongodb.core.mapping.DBRef}s eagerly. + * + * @author Thomas Darimont + */ + class EagerDbRefResolver implements DbRefResolver { + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.DBRefResolver#resolve() + */ + @Override + public Object resolve(MongoPersistentProperty prop, DBObject dbo, final SpELExpressionEvaluator eval, Object parent) { + return getValueInternal(prop, dbo, eval, parent); + } + } + + /** + * A {@link DbRefResolver} that resolves {@link org.springframework.data.mongodb.core.mapping.DBRef}s lazily. + * + * @author Thomas Darimont + */ + class LazyDbRefResolver extends EagerDbRefResolver { + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.DBRefResolver#resolve() + */ + @Override + public Object resolve(final MongoPersistentProperty prop, final DBObject dbo, final SpELExpressionEvaluator eval, + final Object parent) { + + class LazyLoadingInterceptor implements MethodInterceptor { + + volatile boolean initialized; + + Object result; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + if (!initialized) { + initialize(); + } + + return invocation.getMethod().invoke(result, invocation.getArguments()); + } + + private synchronized void initialize() { + + if (!initialized) { + try { + this.result = LazyDbRefResolver.super.resolve(prop, dbo, eval, parent); + this.initialized = true; + } catch (RuntimeException ex) { + throw mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex); + } + cleanupUnnecessaryReferences(); + } + } + + /** + * Cleans up unnecessary references to avoid memory leaks. + */ + private void cleanupUnnecessaryReferences() { + + ReflectionUtils.doWithFields(getClass(), new FieldCallback() { + + @Override + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + if (field.getName().startsWith("val$") || field.getName().startsWith("this$1")) { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, LazyLoadingInterceptor.this, null); + } + } + }); + } + } + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setInterfaces(new Class[] { prop.getRawType() }); + proxyFactory.addAdvice(new LazyLoadingInterceptor()); + + return proxyFactory.getProxy(); + } + } } 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 a5b0267ba6..3272ae41f3 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 @@ * 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 @@ * @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 ae08ed5ea1..0ae20f548c 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 void setUp() { this.template = new MongoTemplate(factory, converter); when(factory.getDb()).thenReturn(db); + when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); when(db.getCollection(Mockito.any(String.class))).thenReturn(collection); } @@ -228,14 +226,12 @@ public void registersDefaultEntityIndexCreatorIfApplicationContextHasOneForDiffe class AutogenerateableId { - @Id - BigInteger id; + @Id BigInteger id; } class NotAutogenerateableId { - @Id - Integer id; + @Id Integer id; public Pattern getId() { return Pattern.compile("."); 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 2a86e1c4f4..31aac2c679 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 @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.repository; import java.util.Date; +import java.util.List; import java.util.Set; import org.springframework.data.mongodb.core.geo.Point; @@ -28,6 +29,7 @@ * Sample domain class. * * @author Oliver Gierke + * @author Thomas Darimont */ @Document public class Person extends Contact { @@ -38,21 +40,19 @@ public enum Sex { private String firstname; private String lastname; - @Indexed(unique = true, dropDups = true) - private String email; + @Indexed(unique = true, dropDups = true) private String email; private Integer age; - @SuppressWarnings("unused") - private Sex sex; + @SuppressWarnings("unused") private Sex sex; Date createdAt; - @GeoSpatialIndexed - private Point location; + @GeoSpatialIndexed private Point location; private Address address; private Set
shippingAddresses; - @DBRef - User creator; + @DBRef User creator; + + @DBRef(lazy = true) List fans; Credentials credentials; @@ -193,6 +193,20 @@ public String getName() { 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; + } + /* * (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 51196811ec..c22eacd926 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 @@ -15,6 +15,13 @@ */ package org.springframework.data.mongodb.repository; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; import org.springframework.test.context.ContextConfiguration; /** @@ -24,4 +31,36 @@ */ @ContextConfiguration public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { + + /** + * @see DATAMONGO-348 + */ + @Test + public void shouldLoadAssociationWithDbRefAndLazyLoading() throws Exception { + + operations.remove(new org.springframework.data.mongodb.core.query.Query(), User.class); + + User thomas = new User(); + thomas.username = "Oliver"; + operations.save(thomas); + + Person person = new Person(); + person.setFirstname("Thomas"); + person.setFans(Arrays.asList(thomas)); + repository.save(person); + + Person oliver = repository.findOne(person.id); + List fans = oliver.getFans(); + // TODO test internal object state of 'fans' before accessing + // initialized should be 'false' + // result should be 'null' + + User user = fans.get(0); + // TODO test internal object state of 'fans' after accessing + // initialized should be 'true' + // result should be not 'null' + // other fields should be 'null' + + assertThat(user.username, is(thomas.username)); + } }