DATAMONGO-1719 - Add support for open/closed interface projections to fluent reactive API.

Original Pull Request: #487
This commit is contained in:
Christoph Strobl
2017-07-21 20:34:02 +02:00
parent 47481c4597
commit 58b33e949b
3 changed files with 266 additions and 5 deletions

View File

@@ -18,6 +18,10 @@ package org.springframework.data.mongodb.core;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.util.ClassUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
@@ -101,6 +105,7 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Pair;
import org.springframework.util.Assert;
@@ -172,6 +177,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final PersistenceExceptionTranslator exceptionTranslator;
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
@@ -214,6 +220,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory();
// We always have a mapping context in the converter, whether it's a simple one or not
mappingContext = this.mongoConverter.getMappingContext();
@@ -281,6 +288,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (mappingContext instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
}
projectionFactory.setBeanFactory(applicationContext);
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
}
/**
@@ -796,7 +806,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
GeoNearResultDbObjectCallback<T> callback = new GeoNearResultDbObjectCallback<T>(
new ReadDocumentCallback<T>(mongoConverter, returnType, collectionName), near.getMetric());
new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collectionName), near.getMetric());
return executeCommand(command, this.readPreference).flatMapMany(document -> {
@@ -1859,7 +1869,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(sourceClass);
Document mappedFields = queryMapper.getMappedFields(fields, entity);
Document mappedFields = getMappedFieldsObject(fields, entity, targetClass);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
if (LOGGER.isDebugEnabled()) {
@@ -1868,7 +1878,36 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer,
new ReadDocumentCallback<T>(mongoConverter, targetClass, collectionName), collectionName);
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
}
private Document getMappedFieldsObject(Document fields, MongoPersistentEntity<?> entity, Class<?> targetType) {
return queryMapper.getMappedFields(addFieldsForProjection(fields, entity.getType(), targetType), entity);
}
/**
* For cases where {@code fields} is {@literal null} or {@literal empty} add fields required for creating the
* projection (target) type if the {@code targetType} is a {@literal closed interface projection}.
*
* @param fields can be {@literal null}.
* @param domainType must not be {@literal null}.
* @param targetType must not be {@literal null}.
* @return {@link Document} with fields to be included.
*/
private Document addFieldsForProjection(Document fields, Class<?> domainType, Class<?> targetType) {
if ((fields != null && !fields.isEmpty()) || !targetType.isInterface()
|| ClassUtils.isAssignable(domainType, targetType)) {
return fields;
}
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
if (projectionInformation.isClosed()) {
projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1));
}
return fields;
}
protected CreateCollectionOptions convertToCreateCollectionOptions(CollectionOptions collectionOptions) {
@@ -2471,6 +2510,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
public T doWith(Document object) {
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<T>(object, type, collectionName));
}
@@ -2482,6 +2522,47 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
}
/**
* {@link MongoTemplate.DocumentCallback} transforming {@link Document} into the given {@code targetType} or
* decorating the {@code sourceType} with a {@literal projection} in case the {@code targetType} is an
* {@litera interface}.
*
* @param <S>
* @param <T>
* @author Christoph Strobl
* @since 2.0
*/
@RequiredArgsConstructor
private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
private final @NonNull EntityReader<Object, Bson> reader;
private final @NonNull Class<S> entityType;
private final @NonNull Class<T> targetType;
private final @NonNull String collectionName;
public T doWith(Document object) {
if (object == null) {
return null;
}
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
: targetType;
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<>(object, typeToRead, collectionName));
}
Object source = reader.read(typeToRead, object);
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
if (null != source) {
maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName));
}
return (T) result;
}
}
/**
* {@link DocumentCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to
* a delegate and creates a {@link GeoResult} from the result.

View File

@@ -25,6 +25,7 @@ import reactor.test.StepVerifier;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
@@ -41,6 +42,7 @@ import com.mongodb.reactivestreams.client.MongoClients;
* Integration tests for {@link ReactiveFindOperationSupport}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public class ReactiveFindOperationSupportTests {
@@ -138,6 +140,29 @@ public class ReactiveFindOperationSupportTests {
.consumeNextWith(it -> assertThat(it).isInstanceOf(Jedi.class)).verifyComplete();
}
@Test // DATAMONGO-1719
public void findAllByWithClosedInterfaceProjection() {
StepVerifier.create(
template.query(Person.class).as(PersonProjection.class).matching(query(where("firstname").is("luke"))).all())
.consumeNextWith(it -> {
assertThat(it).isInstanceOf(PersonProjection.class);
assertThat(it.getFirstname()).isEqualTo("luke");
}).verifyComplete();
}
@Test // DATAMONGO-1719
public void findAllByWithOpenInterfaceProjection() {
StepVerifier.create(template.query(Person.class).as(PersonSpELProjection.class)
.matching(query(where("firstname").is("luke"))).all()).consumeNextWith(it -> {
assertThat(it).isInstanceOf(PersonSpELProjection.class);
assertThat(it.getName()).isEqualTo("luke");
}).verifyComplete();
}
@Test // DATAMONGO-1719
public void findBy() {
@@ -197,6 +222,48 @@ public class ReactiveFindOperationSupportTests {
}).expectNextCount(1).verifyComplete();
}
@Test // DATAMONGO-1719
public void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() {
blocking.indexOps(Planet.class).ensureIndex(
new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538));
Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193));
blocking.save(alderan);
blocking.save(dantooine);
StepVerifier.create(template.query(Planet.class).as(PlanetProjection.class)
.near(NearQuery.near(-73.9667, 40.78).spherical(true)).all()).consumeNextWith(it -> {
assertThat(it.getDistance()).isNotNull();
assertThat(it.getContent()).isInstanceOf(PlanetProjection.class);
assertThat(it.getContent().getName()).isEqualTo("alderan");
}).expectNextCount(1).verifyComplete();
}
@Test // DATAMONGO-1719
public void findAllNearByReturningGeoResultContentAsOpenInterfaceProjection() {
blocking.indexOps(Planet.class).ensureIndex(
new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538));
Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193));
blocking.save(alderan);
blocking.save(dantooine);
StepVerifier.create(template.query(Planet.class).as(PlanetSpELProjection.class)
.near(NearQuery.near(-73.9667, 40.78).spherical(true)).all()).consumeNextWith(it -> {
assertThat(it.getDistance()).isNotNull();
assertThat(it.getContent()).isInstanceOf(PlanetSpELProjection.class);
assertThat(it.getContent().getId()).isEqualTo("alderan");
}).expectNextCount(1).verifyComplete();
}
@Test // DATAMONGO-1719
public void firstShouldReturnFirstEntryInCollection() {
StepVerifier.create(template.query(Person.class).first()).expectNextCount(1).verifyComplete();
@@ -243,13 +310,25 @@ public class ReactiveFindOperationSupportTests {
.expectNext(false).verifyComplete();
}
interface Contact {}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
static class Person {
static class Person implements Contact {
@Id String id;
String firstname;
}
interface PersonProjection {
String getFirstname();
}
public interface PersonSpELProjection {
@Value("#{target.firstname}")
String getName();
}
@Data
static class Human {
@Id String id;
@@ -269,4 +348,14 @@ public class ReactiveFindOperationSupportTests {
@Id String name;
Point coordinates;
}
interface PlanetProjection {
String getName();
}
interface PlanetSpELProjection {
@Value("#{target.name}")
String getId();
}
}

View File

@@ -21,6 +21,7 @@ import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import lombok.Data;
import reactor.core.publisher.Mono;
import org.bson.Document;
@@ -33,13 +34,16 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.test.util.ReflectionTestUtils;
@@ -257,4 +261,91 @@ public class ReactiveMongoTemplateUnitTests {
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
@Test // DATAMONGO-1719
public void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() {
template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, null)
.subscribe();
verify(findPublisher).projection(eq(new Document("firstname", 1)));
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() {
template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, null)
.subscribe();
verify(findPublisher).projection(eq(new Document("bar", 1)));
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, null)
.subscribe();
verify(findPublisher, never()).projection(any());
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsToDtoProjection() {
template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, null).subscribe();
verify(findPublisher, never()).projection(any());
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() {
template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, null).subscribe();
verify(findPublisher).projection(eq(new Document("bar", 1)));
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsWhenTargetIsNotAProjection() {
template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, null).subscribe();
verify(findPublisher, never()).projection(any());
}
@Test // DATAMONGO-1719
public void doesNotApplyFieldsWhenTargetExtendsDomainType() {
template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, null).subscribe();
verify(findPublisher, never()).projection(any());
}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars")
static class Person {
@Id String id;
String firstname;
}
static class PersonExtended extends Person {
String lastname;
}
interface PersonProjection {
String getFirstname();
}
public interface PersonSpELProjection {
@Value("#{target.firstname}")
String getName();
}
@Data
static class Jedi {
@Field("firstname") String name;
}
}