Skip to content

Commit ceecdc4

Browse files
DATAMONGO-1194 - Improve DBRef resolution for collections.
We now bulk load collections of referenced objects as long as they are stored in the same collection. This reduces db roundtrips and network traffic.
1 parent 38288f7 commit ceecdc4

File tree

6 files changed

+388
-13
lines changed

6 files changed

+388
-13
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2015 the original author or authors.
2+
* Copyright 2013-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18+
import java.util.List;
19+
1820
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
1921
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2022

@@ -64,4 +66,14 @@ DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation
6466
* @since 1.7
6567
*/
6668
DBObject fetch(DBRef dbRef);
69+
70+
/**
71+
* Loads a given {@link List} of {@link DBRef}s from the datasource in one batch. <br />
72+
* The {@link DBRef} elements in the list must not reference different collections.
73+
*
74+
* @param dbRefs must not be {@literal null}.
75+
* @return never {@literal null}.
76+
* @since 1.10
77+
*/
78+
List<DBObject> bulkFetch(List<DBRef> dbRefs);
6779
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
import java.io.ObjectOutputStream;
2323
import java.io.Serializable;
2424
import java.lang.reflect.Method;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.Comparator;
28+
import java.util.List;
2529

2630
import org.aopalliance.intercept.MethodInterceptor;
2731
import org.aopalliance.intercept.MethodInvocation;
@@ -31,6 +35,7 @@
3135
import org.springframework.cglib.proxy.Factory;
3236
import org.springframework.cglib.proxy.MethodProxy;
3337
import org.springframework.dao.DataAccessException;
38+
import org.springframework.dao.InvalidDataAccessApiUsageException;
3439
import org.springframework.dao.support.PersistenceExceptionTranslator;
3540
import org.springframework.data.mongodb.LazyLoadingException;
3641
import org.springframework.data.mongodb.MongoDbFactory;
@@ -40,6 +45,9 @@
4045
import org.springframework.util.Assert;
4146
import org.springframework.util.ReflectionUtils;
4247

48+
import com.mongodb.BasicDBObject;
49+
import com.mongodb.BasicDBObjectBuilder;
50+
import com.mongodb.DB;
4351
import com.mongodb.DBObject;
4452
import com.mongodb.DBRef;
4553

@@ -109,6 +117,40 @@ public DBObject fetch(DBRef dbRef) {
109117
return ReflectiveDBRefResolver.fetch(mongoDbFactory, dbRef);
110118
}
111119

120+
/*
121+
* (non-Javadoc)
122+
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#bulkFetch(java.util.List)
123+
*/
124+
@Override
125+
public List<DBObject> bulkFetch(List<DBRef> refs) {
126+
127+
Assert.notNull(mongoDbFactory, "Factory must not be null!");
128+
Assert.notNull(refs, "DBRef to fetch must not be null!");
129+
130+
if (refs.isEmpty()) {
131+
return Collections.emptyList();
132+
}
133+
134+
String collection = refs.iterator().next().getCollectionName();
135+
136+
List<Object> ids = new ArrayList<Object>(refs.size());
137+
for (DBRef ref : refs) {
138+
139+
if (!collection.equals(ref.getCollectionName())) {
140+
throw new InvalidDataAccessApiUsageException(
141+
"DBRefs must all target the same collection for bulk fetch operation.");
142+
}
143+
144+
ids.add(ref.getId());
145+
}
146+
147+
DB db = mongoDbFactory.getDb();
148+
List<DBObject> result = db.getCollection(collection)
149+
.find(new BasicDBObjectBuilder().add("_id", new BasicDBObject("$in", ids)).get()).toArray();
150+
Collections.sort(result, new DbRefByReferencePositionComperator(ids));
151+
return result;
152+
}
153+
112154
/**
113155
* Creates a proxy for the given {@link MongoPersistentProperty} using the given {@link DbRefResolverCallback} to
114156
* eventually resolve the value of the property.
@@ -395,4 +437,25 @@ private synchronized Object resolve() {
395437
return result;
396438
}
397439
}
440+
441+
/**
442+
* {@link Comparator} for sorting {@link DBObject} that have been loaded in random order by a predefined list of
443+
* reference ids.
444+
*
445+
* @author Christoph Strobl
446+
* @since 1.10
447+
*/
448+
private static class DbRefByReferencePositionComperator implements Comparator<DBObject> {
449+
450+
List<Object> reference;
451+
452+
public DbRefByReferencePositionComperator(List<Object> referenceIds) {
453+
reference = new ArrayList<Object>(referenceIds);
454+
}
455+
456+
@Override
457+
public int compare(DBObject o1, DBObject o2) {
458+
return Integer.compare(reference.indexOf(o1.get("_id")), reference.indexOf(o2.get("_id")));
459+
}
460+
}
398461
}

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

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -901,9 +901,14 @@ private Object readCollectionOrArray(TypeInformation<?> targetType, BasicDBList
901901
Collection<Object> items = targetType.getType().isArray() ? new ArrayList<Object>()
902902
: CollectionFactory.createCollection(collectionType, rawComponentType, sourceValue.size());
903903

904+
if (isCollectionOfDbRefWhereBulkFetchIsPossible(sourceValue) && !DBRef.class.equals(rawComponentType)) {
905+
return bulkReadAndConvertDBRefs((List<DBRef>) (ArrayList) sourceValue, componentType, path, rawComponentType);
906+
}
907+
904908
for (Object dbObjItem : sourceValue) {
905909

906910
if (dbObjItem instanceof DBRef) {
911+
907912
items.add(DBRef.class.equals(rawComponentType) ? dbObjItem
908913
: readAndConvertDBRef((DBRef) dbObjItem, componentType, path, rawComponentType));
909914
} else if (dbObjItem instanceof DBObject) {
@@ -916,6 +921,27 @@ private Object readCollectionOrArray(TypeInformation<?> targetType, BasicDBList
916921
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
917922
}
918923

924+
private boolean isCollectionOfDbRefWhereBulkFetchIsPossible(Collection<Object> source) {
925+
926+
String collection = null;
927+
928+
for (Object dbObjItem : source) {
929+
930+
if (!(dbObjItem instanceof DBRef)) {
931+
return false;
932+
}
933+
934+
DBRef ref = (DBRef) dbObjItem;
935+
936+
if (collection != null && !collection.equals(ref.getCollectionName())) {
937+
return false;
938+
}
939+
collection = ref.getCollectionName();
940+
}
941+
942+
return true;
943+
}
944+
919945
/**
920946
* Reads the given {@link DBObject} into a {@link Map}. will recursively resolve nested {@link Map}s as well.
921947
*
@@ -1215,23 +1241,41 @@ private <T> T potentiallyReadOrResolveDbRef(DBRef dbref, TypeInformation<?> type
12151241
return (T) (object != null ? object : readAndConvertDBRef(dbref, type, path, rawType));
12161242
}
12171243

1218-
@SuppressWarnings("unchecked")
12191244
private <T> T readAndConvertDBRef(DBRef dbref, TypeInformation<?> type, ObjectPath path, final Class<?> rawType) {
12201245

1221-
final DBObject readRef = readRef(dbref);
1222-
final String collectionName = dbref.getCollectionName();
1246+
List<T> result = bulkReadAndConvertDBRefs(Collections.singletonList(dbref), type, path, rawType);
1247+
return CollectionUtils.isEmpty(result) ? null : result.iterator().next();
1248+
}
1249+
1250+
@SuppressWarnings("unchecked")
1251+
private <T> List<T> bulkReadAndConvertDBRefs(List<DBRef> dbrefs, TypeInformation<?> type, ObjectPath path,
1252+
final Class<?> rawType) {
12231253

1224-
if (readRef != null) {
1225-
maybeEmitEvent(new AfterLoadEvent<T>(readRef, (Class<T>) rawType, collectionName));
1254+
if (CollectionUtils.isEmpty(dbrefs)) {
1255+
return Collections.emptyList();
12261256
}
12271257

1228-
final T target = (T) read(type, readRef, path);
1258+
final List<DBObject> referencedRawDocuments = dbrefs.size() == 1
1259+
? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs);
1260+
final String collectionName = dbrefs.iterator().next().getCollectionName();
12291261

1230-
if (target != null) {
1231-
maybeEmitEvent(new AfterConvertEvent<T>(readRef, target, collectionName));
1262+
List<T> targeList = new ArrayList<T>(dbrefs.size());
1263+
1264+
for (DBObject document : referencedRawDocuments) {
1265+
1266+
if (document != null) {
1267+
maybeEmitEvent(new AfterLoadEvent<T>(document, (Class<T>) rawType, collectionName));
1268+
}
1269+
1270+
final T target = (T) read(type, document, path);
1271+
targeList.add(target);
1272+
1273+
if (target != null) {
1274+
maybeEmitEvent(new AfterConvertEvent<T>(document, target, collectionName));
1275+
}
12321276
}
12331277

1234-
return target;
1278+
return targeList;
12351279
}
12361280

12371281
private void maybeEmitEvent(MongoMappingEvent<?> event) {
@@ -1255,6 +1299,17 @@ DBObject readRef(DBRef ref) {
12551299
return dbRefResolver.fetch(ref);
12561300
}
12571301

1302+
/**
1303+
* Performs a bulk fetch operation for the given {@link DBRef}s.
1304+
*
1305+
* @param references must not be {@literal null}.
1306+
* @return never {@literal null}.
1307+
* @since 1.10
1308+
*/
1309+
List<DBObject> bulkReadRefs(List<DBRef> references) {
1310+
return dbRefResolver.bulkFetch(references);
1311+
}
1312+
12581313
/**
12591314
* Marker class used to indicate we have a non root document object here that might be used within an update - so we
12601315
* need to preserve type hints for potential nested elements but need to remove it on top level.

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@
9595
import com.mongodb.WriteConcern;
9696
import com.mongodb.WriteResult;
9797

98+
import lombok.Data;
99+
import lombok.EqualsAndHashCode;
100+
import lombok.NoArgsConstructor;
101+
98102
/**
99103
* Integration test for {@link MongoTemplate}.
100104
*
@@ -3344,6 +3348,50 @@ public void updatesBigNumberValueUsingStringComparisonWhenUsingMinOperator() {
33443348
assertThat(loaded.bigDeciamVal, equalTo(new BigDecimal("800")));
33453349
}
33463350

3351+
/**
3352+
* @see DATAMONGO-1194
3353+
*/
3354+
@Test
3355+
public void shouldFetchListOfReferencesCorrectly() {
3356+
3357+
Sample one = new Sample("1", "jon snow");
3358+
Sample two = new Sample("2", "tyrion lannister");
3359+
3360+
template.save(one);
3361+
template.save(two);
3362+
3363+
DocumentWithDBRefCollection source = new DocumentWithDBRefCollection();
3364+
source.dbRefAnnotatedList = Arrays.asList(two, one);
3365+
3366+
template.save(source);
3367+
3368+
assertThat(template.findOne(query(where("id").is(source.id)), DocumentWithDBRefCollection.class), is(source));
3369+
}
3370+
3371+
/**
3372+
* @see DATAMONGO-1194
3373+
*/
3374+
@Test
3375+
public void shouldFetchListOfLazyReferencesCorrectly() {
3376+
3377+
Sample one = new Sample("1", "jon snow");
3378+
Sample two = new Sample("2", "tyrion lannister");
3379+
3380+
template.save(one);
3381+
template.save(two);
3382+
3383+
DocumentWithDBRefCollection source = new DocumentWithDBRefCollection();
3384+
source.lazyDbRefAnnotatedList = Arrays.asList(two, one);
3385+
3386+
template.save(source);
3387+
3388+
DocumentWithDBRefCollection target = template.findOne(query(where("id").is(source.id)),
3389+
DocumentWithDBRefCollection.class);
3390+
3391+
assertThat(target.lazyDbRefAnnotatedList, instanceOf(LazyLoadingProxy.class));
3392+
assertThat(target.getLazyDbRefAnnotatedList(), contains(two, one));
3393+
}
3394+
33473395
static class TypeWithNumbers {
33483396

33493397
@Id String id;
@@ -3403,6 +3451,7 @@ public boolean equals(Object obj) {
34033451

34043452
}
34053453

3454+
@Data
34063455
static class DocumentWithDBRefCollection {
34073456

34083457
@Id public String id;
@@ -3413,6 +3462,10 @@ static class DocumentWithDBRefCollection {
34133462

34143463
@org.springframework.data.mongodb.core.mapping.DBRef //
34153464
public Sample dbRefProperty;
3465+
3466+
@Field("lazy_db_ref_list") /** @see DATAMONGO-1194 */
3467+
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) //
3468+
public List<Sample> lazyDbRefAnnotatedList;
34163469
}
34173470

34183471
static class DocumentWithCollection {
@@ -3504,13 +3557,13 @@ static class TypeWithMyId {
35043557
@Id MyId id;
35053558
}
35063559

3560+
@EqualsAndHashCode
3561+
@NoArgsConstructor
35073562
static class Sample {
35083563

35093564
@Id String id;
35103565
String field;
35113566

3512-
public Sample() {}
3513-
35143567
public Sample(String id, String field) {
35153568
this.id = id;
35163569
this.field = field;
@@ -3705,5 +3758,4 @@ static class WithGeoJson {
37053758
String description;
37063759
GeoJsonPoint point;
37073760
}
3708-
37093761
}

0 commit comments

Comments
 (0)