Skip to content

Commit 0070b12

Browse files
mp911dechristophstrobl
authored andcommitted
Add general support for direct projections.
Closes: #3894
1 parent bafc2be commit 0070b12

26 files changed

+830
-173
lines changed

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@
2323
import org.bson.Document;
2424
import org.springframework.core.convert.ConversionService;
2525
import org.springframework.dao.InvalidDataAccessApiUsageException;
26+
import org.springframework.data.convert.CustomConversions;
2627
import org.springframework.data.mapping.IdentifierAccessor;
2728
import org.springframework.data.mapping.MappingException;
2829
import org.springframework.data.mapping.PersistentEntity;
2930
import org.springframework.data.mapping.PersistentPropertyAccessor;
31+
import org.springframework.data.mapping.context.EntityProjection;
32+
import org.springframework.data.mapping.context.EntityProjectionIntrospector;
3033
import org.springframework.data.mapping.context.MappingContext;
3134
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
3235
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
36+
import org.springframework.data.mongodb.core.convert.MongoConverter;
3337
import org.springframework.data.mongodb.core.convert.MongoWriter;
3438
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3539
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -39,6 +43,7 @@
3943
import org.springframework.data.mongodb.core.query.Criteria;
4044
import org.springframework.data.mongodb.core.query.Query;
4145
import org.springframework.data.mongodb.core.timeseries.Granularity;
46+
import org.springframework.data.projection.ProjectionFactory;
4247
import org.springframework.lang.Nullable;
4348
import org.springframework.util.Assert;
4449
import org.springframework.util.ClassUtils;
@@ -63,8 +68,19 @@ class EntityOperations {
6368

6469
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context;
6570

66-
EntityOperations(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
71+
private final EntityProjectionIntrospector introspector;
72+
73+
EntityOperations(MongoConverter converter) {
74+
this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory());
75+
}
76+
77+
EntityOperations(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
78+
CustomConversions conversions, ProjectionFactory projectionFactory) {
6779
this.context = context;
80+
this.introspector = EntityProjectionIntrospector.create(projectionFactory,
81+
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
82+
.and(((target, underlyingType) -> !conversions.isSimpleType(target))),
83+
context);
6884
}
6985

7086
/**
@@ -229,6 +245,11 @@ public <T> TypedOperations<T> forType(@Nullable Class<T> entityClass) {
229245
return UntypedOperations.instance();
230246
}
231247

248+
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType,
249+
Class<D> entityType) {
250+
return introspector.introspect(resultType, entityType);
251+
}
252+
232253
/**
233254
* A representation of information about an entity.
234255
*

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

+61-33
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.data.geo.Metric;
5050
import org.springframework.data.mapping.MappingException;
5151
import org.springframework.data.mapping.callback.EntityCallbacks;
52+
import org.springframework.data.mapping.context.EntityProjection;
5253
import org.springframework.data.mapping.context.MappingContext;
5354
import org.springframework.data.mongodb.MongoDatabaseFactory;
5455
import org.springframework.data.mongodb.MongoDatabaseUtils;
@@ -102,7 +103,6 @@
102103
import org.springframework.data.mongodb.core.timeseries.Granularity;
103104
import org.springframework.data.mongodb.core.validation.Validator;
104105
import org.springframework.data.mongodb.util.BsonUtils;
105-
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
106106
import org.springframework.data.util.CloseableIterator;
107107
import org.springframework.data.util.Optionals;
108108
import org.springframework.lang.Nullable;
@@ -173,7 +173,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
173173
private final QueryMapper queryMapper;
174174
private final UpdateMapper updateMapper;
175175
private final JsonSchemaMapper schemaMapper;
176-
private final SpelAwareProxyProjectionFactory projectionFactory;
177176
private final EntityOperations operations;
178177
private final PropertyOperations propertyOperations;
179178
private final QueryOperations queryOperations;
@@ -225,8 +224,7 @@ public MongoTemplate(MongoDatabaseFactory mongoDbFactory, @Nullable MongoConvert
225224
this.queryMapper = new QueryMapper(this.mongoConverter);
226225
this.updateMapper = new UpdateMapper(this.mongoConverter);
227226
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
228-
this.projectionFactory = new SpelAwareProxyProjectionFactory();
229-
this.operations = new EntityOperations(this.mongoConverter.getMappingContext());
227+
this.operations = new EntityOperations(this.mongoConverter);
230228
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
231229
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
232230
mongoDbFactory);
@@ -264,7 +262,6 @@ private MongoTemplate(MongoDatabaseFactory dbFactory, MongoTemplate that) {
264262
this.queryMapper = that.queryMapper;
265263
this.updateMapper = that.updateMapper;
266264
this.schemaMapper = that.schemaMapper;
267-
this.projectionFactory = that.projectionFactory;
268265
this.mappingContext = that.mappingContext;
269266
this.operations = that.operations;
270267
this.propertyOperations = that.propertyOperations;
@@ -330,9 +327,6 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
330327
}
331328

332329
resourceLoader = applicationContext;
333-
334-
projectionFactory.setBeanFactory(applicationContext);
335-
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
336330
}
337331

338332
/**
@@ -416,15 +410,17 @@ protected <T> CloseableIterator<T> doStream(Query query, Class<?> entityType, St
416410
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityType);
417411

418412
QueryContext queryContext = queryOperations.createQueryContext(query);
413+
EntityProjection<T, ?> projection = operations.introspectProjection(returnType,
414+
entityType);
419415

420416
Document mappedQuery = queryContext.getMappedQuery(persistentEntity);
421-
Document mappedFields = queryContext.getMappedFields(persistentEntity, returnType, projectionFactory);
417+
Document mappedFields = queryContext.getMappedFields(persistentEntity, projection);
422418

423419
FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection,
424420
col -> col.find(mappedQuery, Document.class).projection(mappedFields));
425421

426422
return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator,
427-
new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName));
423+
new ProjectingReadCallback<>(mongoConverter, projection, collectionName));
428424
});
429425
}
430426

@@ -964,9 +960,11 @@ public <T> GeoResults<T> geoNear(NearQuery near, Class<?> domainType, String col
964960
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
965961

966962
AggregationResults<Document> results = aggregate($geoNear, collection, Document.class);
963+
EntityProjection<T, ?> projection = operations.introspectProjection(returnType,
964+
domainType);
967965

968966
DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<>(distanceField,
969-
new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric());
967+
new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric());
970968

971969
List<GeoResult<T>> result = new ArrayList<>();
972970

@@ -1050,8 +1048,10 @@ public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions
10501048
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityType);
10511049
QueryContext queryContext = queryOperations.createQueryContext(query);
10521050

1051+
EntityProjection<T, S> projection = operations.introspectProjection(resultType,
1052+
entityType);
10531053
Document mappedQuery = queryContext.getMappedQuery(entity);
1054-
Document mappedFields = queryContext.getMappedFields(entity, resultType, projectionFactory);
1054+
Document mappedFields = queryContext.getMappedFields(entity, projection);
10551055
Document mappedSort = queryContext.getMappedSort(entity);
10561056

10571057
replacement = maybeCallBeforeConvert(replacement, collectionName);
@@ -1061,7 +1061,8 @@ public <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions
10611061
maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
10621062

10631063
T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort,
1064-
queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, resultType);
1064+
queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options,
1065+
projection);
10651066

10661067
if (saved != null) {
10671068
maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName));
@@ -2499,7 +2500,8 @@ protected <T> T doFindOne(String collectionName, Document query, Document fields
24992500
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
25002501

25012502
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
2502-
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
2503+
Document mappedFields = queryContext.getMappedFields(entity,
2504+
EntityProjection.nonProjecting(entityClass));
25032505
Document mappedQuery = queryContext.getMappedQuery(entity);
25042506

25052507
if (LOGGER.isDebugEnabled()) {
@@ -2551,7 +2553,8 @@ protected <S, T> List<T> doFind(String collectionName, Document query, Document
25512553
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
25522554

25532555
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
2554-
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
2556+
Document mappedFields = queryContext.getMappedFields(entity,
2557+
EntityProjection.nonProjecting(entityClass));
25552558
Document mappedQuery = queryContext.getMappedQuery(entity);
25562559

25572560
if (LOGGER.isDebugEnabled()) {
@@ -2573,9 +2576,11 @@ <S, T> List<T> doFind(String collectionName, Document query, Document fields, Cl
25732576
Class<T> targetClass, CursorPreparer preparer) {
25742577

25752578
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(sourceClass);
2579+
EntityProjection<T, S> projection = operations.introspectProjection(targetClass,
2580+
sourceClass);
25762581

25772582
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
2578-
Document mappedFields = queryContext.getMappedFields(entity, targetClass, projectionFactory);
2583+
Document mappedFields = queryContext.getMappedFields(entity, projection);
25792584
Document mappedQuery = queryContext.getMappedQuery(entity);
25802585

25812586
if (LOGGER.isDebugEnabled()) {
@@ -2584,9 +2589,10 @@ <S, T> List<T> doFind(String collectionName, Document query, Document fields, Cl
25842589
}
25852590

25862591
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer,
2587-
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
2592+
new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
25882593
}
25892594

2595+
25902596
/**
25912597
* Convert given {@link CollectionOptions} to a document and take the domain type information into account when
25922598
* creating a mapped schema for validation. <br />
@@ -2745,6 +2751,35 @@ protected <T> T doFindAndReplace(String collectionName, Document mappedQuery, Do
27452751
Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class<?> entityType,
27462752
Document replacement, FindAndReplaceOptions options, Class<T> resultType) {
27472753

2754+
EntityProjection<T, ?> projection = operations.introspectProjection(resultType,
2755+
entityType);
2756+
2757+
return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement,
2758+
options, projection);
2759+
}
2760+
2761+
/**
2762+
* Customize this part for findAndReplace.
2763+
*
2764+
* @param collectionName The name of the collection to perform the operation in.
2765+
* @param mappedQuery the query to look up documents.
2766+
* @param mappedFields the fields to project the result to.
2767+
* @param mappedSort the sort to be applied when executing the query.
2768+
* @param collation collation settings for the query. Can be {@literal null}.
2769+
* @param entityType the source domain type.
2770+
* @param replacement the replacement {@link Document}.
2771+
* @param options applicable options.
2772+
* @param projection the projection descriptor.
2773+
* @return {@literal null} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is
2774+
* {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
2775+
* @since 3.4
2776+
*/
2777+
@Nullable
2778+
private <T> T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields,
2779+
Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class<?> entityType,
2780+
Document replacement, FindAndReplaceOptions options,
2781+
EntityProjection<T, ?> projection) {
2782+
27482783
if (LOGGER.isDebugEnabled()) {
27492784
LOGGER.debug(String.format(
27502785
"findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " + "in collection: %s",
@@ -2754,7 +2789,7 @@ protected <T> T doFindAndReplace(String collectionName, Document mappedQuery, Do
27542789

27552790
return executeFindOneInternal(
27562791
new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options),
2757-
new ProjectingReadCallback<>(mongoConverter, entityType, resultType, collectionName), collectionName);
2792+
new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
27582793
}
27592794

27602795
/**
@@ -3205,17 +3240,15 @@ public T doWith(Document document) {
32053240
*/
32063241
private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
32073242

3208-
private final EntityReader<Object, Bson> reader;
3209-
private final Class<S> entityType;
3210-
private final Class<T> targetType;
3243+
private final MongoConverter reader;
3244+
private final EntityProjection<T, S> projection;
32113245
private final String collectionName;
32123246

3213-
ProjectingReadCallback(EntityReader<Object, Bson> reader, Class<S> entityType, Class<T> targetType,
3247+
ProjectingReadCallback(MongoConverter reader, EntityProjection<T, S> projection,
32143248
String collectionName) {
32153249

32163250
this.reader = reader;
3217-
this.entityType = entityType;
3218-
this.targetType = targetType;
3251+
this.projection = projection;
32193252
this.collectionName = collectionName;
32203253
}
32213254

@@ -3230,21 +3263,16 @@ public T doWith(Document document) {
32303263
return null;
32313264
}
32323265

3233-
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
3234-
: targetType;
3266+
maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName));
32353267

3236-
maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName));
3237-
3238-
Object entity = reader.read(typeToRead, document);
3268+
Object entity = reader.project(projection, document);
32393269

32403270
if (entity == null) {
32413271
throw new MappingException(String.format("EntityReader %s returned null", reader));
32423272
}
32433273

3244-
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity;
3245-
3246-
maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName));
3247-
return (T) maybeCallAfterConvert(result, document, collectionName);
3274+
maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName));
3275+
return (T) maybeCallAfterConvert(entity, document, collectionName);
32483276
}
32493277
}
32503278

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

+23-22
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616
package org.springframework.data.mongodb.core;
1717

1818
import org.bson.Document;
19-
import org.springframework.data.mapping.SimplePropertyHandler;
19+
20+
import org.springframework.data.mapping.context.EntityProjection;
2021
import org.springframework.data.mapping.context.MappingContext;
2122
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2223
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
23-
import org.springframework.data.projection.ProjectionFactory;
24-
import org.springframework.data.projection.ProjectionInformation;
25-
import org.springframework.util.ClassUtils;
24+
import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator;
25+
import org.springframework.data.util.Predicates;
2626

2727
/**
2828
* Common operations performed on properties of an entity like extracting fields information for projection creation.
2929
*
3030
* @author Christoph Strobl
31+
* @author Mark Paluch
3132
* @since 2.1
3233
*/
3334
class PropertyOperations {
@@ -40,37 +41,37 @@ class PropertyOperations {
4041

4142
/**
4243
* For cases where {@code fields} is {@link Document#isEmpty() empty} include only fields that are required for
43-
* creating the projection (target) type if the {@code targetType} is a {@literal DTO projection} or a
44+
* creating the projection (target) type if the {@code EntityProjection} is a {@literal DTO projection} or a
4445
* {@literal closed interface projection}.
4546
*
46-
* @param projectionFactory must not be {@literal null}.
47+
* @param projection must not be {@literal null}.
4748
* @param fields must not be {@literal null}.
48-
* @param domainType must not be {@literal null}.
49-
* @param targetType must not be {@literal null}.
5049
* @return {@link Document} with fields to be included.
5150
*/
52-
Document computeFieldsForProjection(ProjectionFactory projectionFactory, Document fields, Class<?> domainType,
53-
Class<?> targetType) {
51+
Document computeMappedFieldsForProjection(EntityProjection<?, ?> projection,
52+
Document fields) {
5453

55-
if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) {
54+
if (!projection.isProjection() || !projection.isClosedProjection()) {
5655
return fields;
5756
}
5857

5958
Document projectedFields = new Document();
6059

61-
if (targetType.isInterface()) {
62-
63-
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
64-
65-
if (projectionInformation.isClosed()) {
66-
projectionInformation.getInputProperties().forEach(it -> projectedFields.append(it.getName(), 1));
67-
}
60+
if (projection.getMappedType().getType().isInterface()) {
61+
projection.forEach(it -> {
62+
projectedFields.put(it.getPropertyPath().getSegment(), 1);
63+
});
6864
} else {
6965

70-
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetType);
71-
if (entity != null) {
72-
entity.doWithProperties(
73-
(SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1));
66+
// DTO projections use merged metadata between domain type and result type
67+
PersistentPropertyTranslator translator = PersistentPropertyTranslator.create(
68+
mappingContext.getRequiredPersistentEntity(projection.getDomainType()),
69+
Predicates.negate(MongoPersistentProperty::hasExplicitFieldName));
70+
71+
MongoPersistentEntity<?> persistentEntity = mappingContext
72+
.getRequiredPersistentEntity(projection.getMappedType());
73+
for (MongoPersistentProperty property : persistentEntity) {
74+
projectedFields.put(translator.translate(property).getFieldName(), 1);
7475
}
7576
}
7677

0 commit comments

Comments
 (0)