DATAMONGO-1719 - Add support for open/closed interface projections to fluent reactive API.
Original Pull Request: #487
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user