Skip to content

Commit 8e4c28f

Browse files
DATAMONGO-1719 - Add support for open/closed interface projections to fluent reactive API.
1 parent 52ffc68 commit 8e4c28f

File tree

3 files changed

+266
-5
lines changed

3 files changed

+266
-5
lines changed

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

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import static org.springframework.data.mongodb.core.query.Criteria.*;
1919
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
2020

21+
import lombok.NonNull;
22+
import lombok.RequiredArgsConstructor;
23+
import org.springframework.data.projection.ProjectionInformation;
24+
import org.springframework.util.ClassUtils;
2125
import reactor.core.publisher.Flux;
2226
import reactor.core.publisher.Mono;
2327
import reactor.util.function.Tuple2;
@@ -101,6 +105,7 @@
101105
import org.springframework.data.mongodb.core.query.Query;
102106
import org.springframework.data.mongodb.core.query.Update;
103107
import org.springframework.data.mongodb.util.MongoClientVersion;
108+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
104109
import org.springframework.data.util.Optionals;
105110
import org.springframework.data.util.Pair;
106111
import org.springframework.util.Assert;
@@ -172,6 +177,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
172177
private final PersistenceExceptionTranslator exceptionTranslator;
173178
private final QueryMapper queryMapper;
174179
private final UpdateMapper updateMapper;
180+
private final SpelAwareProxyProjectionFactory projectionFactory;
175181

176182
private WriteConcern writeConcern;
177183
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
@@ -214,6 +220,7 @@ public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory,
214220
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter;
215221
this.queryMapper = new QueryMapper(this.mongoConverter);
216222
this.updateMapper = new UpdateMapper(this.mongoConverter);
223+
this.projectionFactory = new SpelAwareProxyProjectionFactory();
217224

218225
// We always have a mapping context in the converter, whether it's a simple one or not
219226
mappingContext = this.mongoConverter.getMappingContext();
@@ -281,6 +288,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
281288
if (mappingContext instanceof ApplicationEventPublisherAware) {
282289
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
283290
}
291+
292+
projectionFactory.setBeanFactory(applicationContext);
293+
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
284294
}
285295

286296
/**
@@ -796,7 +806,7 @@ protected <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<?> entityClass, S
796806
}
797807

798808
GeoNearResultDbObjectCallback<T> callback = new GeoNearResultDbObjectCallback<T>(
799-
new ReadDocumentCallback<T>(mongoConverter, returnType, collectionName), near.getMetric());
809+
new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collectionName), near.getMetric());
800810

801811
return executeCommand(command, this.readPreference).flatMapMany(document -> {
802812

@@ -1859,7 +1869,7 @@ <S, T> Flux<T> doFind(String collectionName, Document query, Document fields, Cl
18591869

18601870
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(sourceClass);
18611871

1862-
Document mappedFields = queryMapper.getMappedFields(fields, entity);
1872+
Document mappedFields = getMappedFieldsObject(fields, entity, targetClass);
18631873
Document mappedQuery = queryMapper.getMappedObject(query, entity);
18641874

18651875
if (LOGGER.isDebugEnabled()) {
@@ -1868,7 +1878,36 @@ <S, T> Flux<T> doFind(String collectionName, Document query, Document fields, Cl
18681878
}
18691879

18701880
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer,
1871-
new ReadDocumentCallback<T>(mongoConverter, targetClass, collectionName), collectionName);
1881+
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
1882+
}
1883+
1884+
private Document getMappedFieldsObject(Document fields, MongoPersistentEntity<?> entity, Class<?> targetType) {
1885+
return queryMapper.getMappedFields(addFieldsForProjection(fields, entity.getType(), targetType), entity);
1886+
}
1887+
1888+
/**
1889+
* For cases where {@code fields} is {@literal null} or {@literal empty} add fields required for creating the
1890+
* projection (target) type if the {@code targetType} is a {@literal closed interface projection}.
1891+
*
1892+
* @param fields can be {@literal null}.
1893+
* @param domainType must not be {@literal null}.
1894+
* @param targetType must not be {@literal null}.
1895+
* @return {@link Document} with fields to be included.
1896+
*/
1897+
private Document addFieldsForProjection(Document fields, Class<?> domainType, Class<?> targetType) {
1898+
1899+
if ((fields != null && !fields.isEmpty()) || !targetType.isInterface()
1900+
|| ClassUtils.isAssignable(domainType, targetType)) {
1901+
return fields;
1902+
}
1903+
1904+
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
1905+
1906+
if (projectionInformation.isClosed()) {
1907+
projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1));
1908+
}
1909+
1910+
return fields;
18721911
}
18731912

18741913
protected CreateCollectionOptions convertToCreateCollectionOptions(CollectionOptions collectionOptions) {
@@ -2471,6 +2510,7 @@ class ReadDocumentCallback<T> implements DocumentCallback<T> {
24712510
}
24722511

24732512
public T doWith(Document object) {
2513+
24742514
if (null != object) {
24752515
maybeEmitEvent(new AfterLoadEvent<T>(object, type, collectionName));
24762516
}
@@ -2482,6 +2522,47 @@ public T doWith(Document object) {
24822522
}
24832523
}
24842524

2525+
/**
2526+
* {@link MongoTemplate.DocumentCallback} transforming {@link Document} into the given {@code targetType} or
2527+
* decorating the {@code sourceType} with a {@literal projection} in case the {@code targetType} is an
2528+
* {@litera interface}.
2529+
*
2530+
* @param <S>
2531+
* @param <T>
2532+
* @author Christoph Strobl
2533+
* @since 2.0
2534+
*/
2535+
@RequiredArgsConstructor
2536+
private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
2537+
2538+
private final @NonNull EntityReader<Object, Bson> reader;
2539+
private final @NonNull Class<S> entityType;
2540+
private final @NonNull Class<T> targetType;
2541+
private final @NonNull String collectionName;
2542+
2543+
public T doWith(Document object) {
2544+
2545+
if (object == null) {
2546+
return null;
2547+
}
2548+
2549+
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
2550+
: targetType;
2551+
2552+
if (null != object) {
2553+
maybeEmitEvent(new AfterLoadEvent<>(object, typeToRead, collectionName));
2554+
}
2555+
2556+
Object source = reader.read(typeToRead, object);
2557+
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
2558+
2559+
if (null != source) {
2560+
maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName));
2561+
}
2562+
return (T) result;
2563+
}
2564+
}
2565+
24852566
/**
24862567
* {@link DocumentCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to
24872568
* a delegate and creates a {@link GeoResult} from the result.

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

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.junit.Before;
2727
import org.junit.Test;
28+
import org.springframework.beans.factory.annotation.Value;
2829
import org.springframework.dao.IncorrectResultSizeDataAccessException;
2930
import org.springframework.data.annotation.Id;
3031
import org.springframework.data.geo.Point;
@@ -41,6 +42,7 @@
4142
* Integration tests for {@link ReactiveFindOperationSupport}.
4243
*
4344
* @author Mark Paluch
45+
* @author Christoph Strobl
4446
*/
4547
public class ReactiveFindOperationSupportTests {
4648

@@ -138,6 +140,29 @@ public void findAllByWithProjection() {
138140
.consumeNextWith(it -> assertThat(it).isInstanceOf(Jedi.class)).verifyComplete();
139141
}
140142

143+
@Test // DATAMONGO-1719
144+
public void findAllByWithClosedInterfaceProjection() {
145+
146+
StepVerifier.create(
147+
template.query(Person.class).as(PersonProjection.class).matching(query(where("firstname").is("luke"))).all())
148+
.consumeNextWith(it -> {
149+
150+
assertThat(it).isInstanceOf(PersonProjection.class);
151+
assertThat(it.getFirstname()).isEqualTo("luke");
152+
}).verifyComplete();
153+
}
154+
155+
@Test // DATAMONGO-1719
156+
public void findAllByWithOpenInterfaceProjection() {
157+
158+
StepVerifier.create(template.query(Person.class).as(PersonSpELProjection.class)
159+
.matching(query(where("firstname").is("luke"))).all()).consumeNextWith(it -> {
160+
161+
assertThat(it).isInstanceOf(PersonSpELProjection.class);
162+
assertThat(it.getName()).isEqualTo("luke");
163+
}).verifyComplete();
164+
}
165+
141166
@Test // DATAMONGO-1719
142167
public void findBy() {
143168

@@ -197,6 +222,48 @@ public void findAllNearByWithCollectionAndProjection() {
197222
}).expectNextCount(1).verifyComplete();
198223
}
199224

225+
@Test // DATAMONGO-1719
226+
public void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() {
227+
228+
blocking.indexOps(Planet.class).ensureIndex(
229+
new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
230+
231+
Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538));
232+
Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193));
233+
234+
blocking.save(alderan);
235+
blocking.save(dantooine);
236+
237+
StepVerifier.create(template.query(Planet.class).as(PlanetProjection.class)
238+
.near(NearQuery.near(-73.9667, 40.78).spherical(true)).all()).consumeNextWith(it -> {
239+
240+
assertThat(it.getDistance()).isNotNull();
241+
assertThat(it.getContent()).isInstanceOf(PlanetProjection.class);
242+
assertThat(it.getContent().getName()).isEqualTo("alderan");
243+
}).expectNextCount(1).verifyComplete();
244+
}
245+
246+
@Test // DATAMONGO-1719
247+
public void findAllNearByReturningGeoResultContentAsOpenInterfaceProjection() {
248+
249+
blocking.indexOps(Planet.class).ensureIndex(
250+
new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
251+
252+
Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538));
253+
Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193));
254+
255+
blocking.save(alderan);
256+
blocking.save(dantooine);
257+
258+
StepVerifier.create(template.query(Planet.class).as(PlanetSpELProjection.class)
259+
.near(NearQuery.near(-73.9667, 40.78).spherical(true)).all()).consumeNextWith(it -> {
260+
261+
assertThat(it.getDistance()).isNotNull();
262+
assertThat(it.getContent()).isInstanceOf(PlanetSpELProjection.class);
263+
assertThat(it.getContent().getId()).isEqualTo("alderan");
264+
}).expectNextCount(1).verifyComplete();
265+
}
266+
200267
@Test // DATAMONGO-1719
201268
public void firstShouldReturnFirstEntryInCollection() {
202269
StepVerifier.create(template.query(Person.class).first()).expectNextCount(1).verifyComplete();
@@ -243,13 +310,25 @@ public void existsShouldReturnFalseWhenNoElementMatchesQuery() {
243310
.expectNext(false).verifyComplete();
244311
}
245312

313+
interface Contact {}
314+
246315
@Data
247316
@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
248-
static class Person {
317+
static class Person implements Contact {
249318
@Id String id;
250319
String firstname;
251320
}
252321

322+
interface PersonProjection {
323+
String getFirstname();
324+
}
325+
326+
public interface PersonSpELProjection {
327+
328+
@Value("#{target.firstname}")
329+
String getName();
330+
}
331+
253332
@Data
254333
static class Human {
255334
@Id String id;
@@ -269,4 +348,14 @@ static class Planet {
269348
@Id String name;
270349
Point coordinates;
271350
}
351+
352+
interface PlanetProjection {
353+
String getName();
354+
}
355+
356+
interface PlanetSpELProjection {
357+
358+
@Value("#{target.name}")
359+
String getId();
360+
}
272361
}

0 commit comments

Comments
 (0)