Skip to content

Commit 0e89539

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for direct and nested projections.
Projections are now fully supported from within the CassandraConverter that can materialize DTO and interface projections without using intermediate entities. Closes: #1202
1 parent 1c02d4c commit 0e89539

18 files changed

+615
-93
lines changed

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java

+7-16
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.springframework.data.cassandra.core.query.Query;
5656
import org.springframework.data.domain.Slice;
5757
import org.springframework.data.mapping.callback.EntityCallbacks;
58+
import org.springframework.data.projection.EntityProjection;
5859
import org.springframework.data.projection.ProjectionFactory;
5960
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
6061
import org.springframework.data.util.Streamable;
@@ -119,8 +120,6 @@ public class AsyncCassandraTemplate
119120

120121
private final EntityOperations entityOperations;
121122

122-
private final SpelAwareProxyProjectionFactory projectionFactory;
123-
124123
private final StatementFactory statementFactory;
125124

126125
private @Nullable ApplicationEventPublisher eventPublisher;
@@ -186,9 +185,8 @@ public AsyncCassandraTemplate(AsyncCqlTemplate asyncCqlTemplate, CassandraConver
186185

187186
this.converter = converter;
188187
this.cqlOperations = asyncCqlTemplate;
189-
this.entityOperations = new EntityOperations(converter.getMappingContext());
188+
this.entityOperations = new EntityOperations(converter);
190189
this.exceptionTranslator = asyncCqlTemplate.getExceptionTranslator();
191-
this.projectionFactory = new SpelAwareProxyProjectionFactory();
192190
this.statementFactory = new StatementFactory(converter);
193191
}
194192

@@ -209,9 +207,6 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
209207
if (entityCallbacks == null) {
210208
setEntityCallbacks(EntityCallbacks.create(applicationContext));
211209
}
212-
213-
projectionFactory.setBeanFactory(applicationContext);
214-
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
215210
}
216211

217212
/**
@@ -284,9 +279,11 @@ protected EntityOperations getEntityOperations() {
284279
* projections.
285280
* @see org.springframework.data.projection.SpelAwareProxyProjectionFactory
286281
* @since 2.1
282+
* @deprecated since 3.4, use {@link CassandraConverter#getProjectionFactory()} instead.
287283
*/
284+
@Deprecated
288285
protected SpelAwareProxyProjectionFactory getProjectionFactory() {
289-
return this.projectionFactory;
286+
return (SpelAwareProxyProjectionFactory) getConverter().getProjectionFactory();
290287
}
291288

292289
private CassandraPersistentEntity<?> getRequiredPersistentEntity(Class<?> entityType) {
@@ -953,15 +950,13 @@ public String getCql() {
953950
@SuppressWarnings("unchecked")
954951
private <T> Function<Row, T> getMapper(Class<?> entityType, Class<T> targetType, CqlIdentifier tableName) {
955952

956-
Class<?> typeToRead = resolveTypeToRead(entityType, targetType);
953+
EntityProjection<T, ?> projection = entityOperations.introspectProjection(targetType, entityType);
957954

958955
return row -> {
959956

960957
maybeEmitEvent(new AfterLoadEvent<>(row, targetType, tableName));
961958

962-
Object source = getConverter().read(typeToRead, row);
963-
964-
T result = (T) (targetType.isInterface() ? getProjectionFactory().createProjection(targetType, source) : source);
959+
T result = getConverter().project(projection, row);
965960

966961
if (result != null) {
967962
maybeEmitEvent(new AfterConvertEvent<>(row, result, tableName));
@@ -971,10 +966,6 @@ private <T> Function<Row, T> getMapper(Class<?> entityType, Class<T> targetType,
971966
};
972967
}
973968

974-
private Class<?> resolveTypeToRead(Class<?> entityType, Class<?> targetType) {
975-
return targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType;
976-
}
977-
978969
private static MappingCassandraConverter newConverter(CqlSession session) {
979970

980971
MappingCassandraConverter converter = new MappingCassandraConverter();

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java

+20-26
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.springframework.data.cassandra.core.query.Query;
5656
import org.springframework.data.domain.Slice;
5757
import org.springframework.data.mapping.callback.EntityCallbacks;
58+
import org.springframework.data.projection.EntityProjection;
5859
import org.springframework.data.projection.ProjectionFactory;
5960
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
6061
import org.springframework.lang.Nullable;
@@ -115,8 +116,6 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP
115116

116117
private final EntityOperations entityOperations;
117118

118-
private final SpelAwareProxyProjectionFactory projectionFactory;
119-
120119
private final StatementFactory statementFactory;
121120

122121
private @Nullable ApplicationEventPublisher eventPublisher;
@@ -182,8 +181,7 @@ public CassandraTemplate(CqlOperations cqlOperations, CassandraConverter convert
182181

183182
this.converter = converter;
184183
this.cqlOperations = cqlOperations;
185-
this.entityOperations = new EntityOperations(converter.getMappingContext());
186-
this.projectionFactory = new SpelAwareProxyProjectionFactory();
184+
this.entityOperations = new EntityOperations(converter);
187185
this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter));
188186
}
189187

@@ -212,9 +210,6 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
212210
if (entityCallbacks == null) {
213211
setEntityCallbacks(EntityCallbacks.create(applicationContext));
214212
}
215-
216-
projectionFactory.setBeanFactory(applicationContext);
217-
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
218213
}
219214

220215
/**
@@ -287,9 +282,10 @@ protected EntityOperations getEntityOperations() {
287282
* projections.
288283
* @see org.springframework.data.projection.SpelAwareProxyProjectionFactory
289284
* @since 2.1
285+
* @deprecated since 3.4, use {@link CassandraConverter#getProjectionFactory()} instead.
290286
*/
291287
protected SpelAwareProxyProjectionFactory getProjectionFactory() {
292-
return this.projectionFactory;
288+
return (SpelAwareProxyProjectionFactory) getConverter().getProjectionFactory();
293289
}
294290

295291
private CassandraPersistentEntity<?> getRequiredPersistentEntity(Class<?> entityType) {
@@ -378,7 +374,8 @@ public <T> List<T> select(Statement<?> statement, Class<T> entityClass) {
378374
Assert.notNull(statement, "Statement must not be null");
379375
Assert.notNull(entityClass, "Entity type must not be null");
380376

381-
Function<Row, T> mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
377+
Function<Row, T> mapper = getMapper(EntityProjection.nonProjecting(entityClass),
378+
EntityQueryUtils.getTableName(statement));
382379

383380
return doQuery(statement, (row, rowNum) -> mapper.apply(row));
384381
}
@@ -404,7 +401,8 @@ public <T> Slice<T> slice(Statement<?> statement, Class<T> entityClass) {
404401

405402
ResultSet resultSet = doQueryForResultSet(statement);
406403

407-
Function<Row, T> mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
404+
Function<Row, T> mapper = getMapper(EntityProjection.nonProjecting(entityClass),
405+
EntityQueryUtils.getTableName(statement));
408406

409407
return EntityQueryUtils.readSlice(resultSet, (row, rowNum) -> mapper.apply(row), 0,
410408
getEffectivePageSize(statement));
@@ -419,7 +417,8 @@ public <T> Stream<T> stream(Statement<?> statement, Class<T> entityClass) throws
419417
Assert.notNull(statement, "Statement must not be null");
420418
Assert.notNull(entityClass, "Entity type must not be null");
421419

422-
Function<Row, T> mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
420+
Function<Row, T> mapper = getMapper(EntityProjection.nonProjecting(entityClass),
421+
EntityQueryUtils.getTableName(statement));
423422
return doQueryForStream(statement, (row, rowNum) -> mapper.apply(row));
424423
}
425424

@@ -442,14 +441,14 @@ public <T> List<T> select(Query query, Class<T> entityClass) throws DataAccessEx
442441
<T> List<T> doSelect(Query query, Class<?> entityClass, CqlIdentifier tableName, Class<T> returnType) {
443442

444443
CassandraPersistentEntity<?> entity = getRequiredPersistentEntity(entityClass);
445-
446-
Columns columns = getStatementFactory().computeColumnsForProjection(query.getColumns(), entity, returnType);
444+
EntityProjection<T, ?> projection = entityOperations.introspectProjection(returnType, entityClass);
445+
Columns columns = getStatementFactory().computeColumnsForProjection(projection, query.getColumns(), entity,
446+
returnType);
447447

448448
Query queryToUse = query.columns(columns);
449449

450450
StatementBuilder<Select> select = getStatementFactory().select(queryToUse, entity, tableName);
451-
452-
Function<Row, T> mapper = getMapper(entityClass, returnType, tableName);
451+
Function<Row, T> mapper = getMapper(projection, tableName);
453452

454453
return doQuery(select.build(), (row, rowNum) -> mapper.apply(row));
455454
}
@@ -495,8 +494,9 @@ <T> Stream<T> doStream(Query query, Class<?> entityClass, CqlIdentifier tableNam
495494

496495
StatementBuilder<Select> select = getStatementFactory().select(query, getRequiredPersistentEntity(entityClass),
497496
tableName);
497+
EntityProjection<T, ?> projection = entityOperations.introspectProjection(returnType, entityClass);
498498

499-
Function<Row, T> mapper = getMapper(entityClass, returnType, tableName);
499+
Function<Row, T> mapper = getMapper(projection, tableName);
500500
return doQueryForStream(select.build(), (row, rowNum) -> mapper.apply(row));
501501
}
502502

@@ -639,7 +639,7 @@ public <T> T selectOneById(Object id, Class<T> entityClass) {
639639
CassandraPersistentEntity<?> entity = getRequiredPersistentEntity(entityClass);
640640
CqlIdentifier tableName = entity.getTableName();
641641
StatementBuilder<Select> select = getStatementFactory().selectOneById(id, entity, tableName);
642-
Function<Row, T> mapper = getMapper(entityClass, entityClass, tableName);
642+
Function<Row, T> mapper = getMapper(EntityProjection.nonProjecting(entityClass), tableName);
643643
List<T> result = doQuery(select.build(), (row, rowNum) -> mapper.apply(row));
644644

645645
return result.isEmpty() ? null : result.get(0);
@@ -999,17 +999,15 @@ public String getCql() {
999999
}
10001000

10011001
@SuppressWarnings("unchecked")
1002-
private <T> Function<Row, T> getMapper(Class<?> entityType, Class<T> targetType, CqlIdentifier tableName) {
1002+
private <T> Function<Row, T> getMapper(EntityProjection<T, ?> projection, CqlIdentifier tableName) {
10031003

1004-
Class<?> typeToRead = resolveTypeToRead(entityType, targetType);
1004+
Class<T> targetType = projection.getMappedType().getType();
10051005

10061006
return row -> {
10071007

10081008
maybeEmitEvent(new AfterLoadEvent<>(row, targetType, tableName));
10091009

1010-
Object source = getConverter().read(typeToRead, row);
1011-
1012-
T result = (T) (targetType.isInterface() ? getProjectionFactory().createProjection(targetType, source) : source);
1010+
T result = getConverter().project(projection, row);
10131011

10141012
if (result != null) {
10151013
maybeEmitEvent(new AfterConvertEvent<>(row, result, tableName));
@@ -1019,10 +1017,6 @@ private <T> Function<Row, T> getMapper(Class<?> entityType, Class<T> targetType,
10191017
};
10201018
}
10211019

1022-
private Class<?> resolveTypeToRead(Class<?> entityType, Class<?> targetType) {
1023-
return targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType;
1024-
}
1025-
10261020
private static MappingCassandraConverter newConverter(CqlSession session) {
10271021

10281022
MappingCassandraConverter converter = new MappingCassandraConverter();

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

+20-3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
package org.springframework.data.cassandra.core;
1717

1818
import org.springframework.core.convert.ConversionService;
19+
import org.springframework.data.cassandra.core.convert.CassandraConverter;
1920
import org.springframework.data.cassandra.core.cql.util.StatementBuilder;
2021
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
2122
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
23+
import org.springframework.data.convert.CustomConversions;
2224
import org.springframework.data.mapping.PersistentPropertyAccessor;
2325
import org.springframework.data.mapping.context.MappingContext;
2426
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
27+
import org.springframework.data.projection.EntityProjection;
28+
import org.springframework.data.projection.EntityProjectionIntrospector;
29+
import org.springframework.data.projection.ProjectionFactory;
2530
import org.springframework.lang.Nullable;
2631
import org.springframework.util.Assert;
2732
import org.springframework.util.ClassUtils;
@@ -44,10 +49,19 @@
4449
class EntityOperations {
4550

4651
private final MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext;
52+
private final EntityProjectionIntrospector introspector;
4753

48-
public EntityOperations(
49-
MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
50-
this.mappingContext = mappingContext;
54+
EntityOperations(CassandraConverter converter) {
55+
this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory());
56+
}
57+
58+
EntityOperations(MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> context,
59+
CustomConversions conversions, ProjectionFactory projectionFactory) {
60+
this.mappingContext = context;
61+
this.introspector = EntityProjectionIntrospector.create(projectionFactory,
62+
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
63+
.and(((target, underlyingType) -> !conversions.isSimpleType(target))),
64+
context);
5165
}
5266

5367
/**
@@ -99,6 +113,9 @@ CqlIdentifier getTableName(Class<?> entityClass) {
99113
return getRequiredPersistentEntity(entityClass).getTableName();
100114
}
101115

116+
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> entityType) {
117+
return introspector.introspect(resultType, entityType);
118+
}
102119

103120
protected MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> getMappingContext() {
104121
return this.mappingContext;

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java

+14-22
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.cassandra.core;
1717

18+
import org.springframework.data.projection.EntityProjection;
1819
import reactor.core.publisher.Flux;
1920
import reactor.core.publisher.Mono;
2021
import reactor.core.publisher.SynchronousSink;
@@ -121,8 +122,6 @@ public class ReactiveCassandraTemplate
121122

122123
private final EntityOperations entityOperations;
123124

124-
private final SpelAwareProxyProjectionFactory projectionFactory;
125-
126125
private final StatementFactory statementFactory;
127126

128127
private @Nullable ApplicationEventPublisher eventPublisher;
@@ -189,8 +188,7 @@ public ReactiveCassandraTemplate(ReactiveCqlOperations reactiveCqlOperations, Ca
189188

190189
this.converter = converter;
191190
this.cqlOperations = reactiveCqlOperations;
192-
this.entityOperations = new EntityOperations(converter.getMappingContext());
193-
this.projectionFactory = new SpelAwareProxyProjectionFactory();
191+
this.entityOperations = new EntityOperations(converter);
194192
this.statementFactory = new StatementFactory(converter);
195193
}
196194

@@ -219,9 +217,6 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
219217
if (entityCallbacks == null) {
220218
setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext));
221219
}
222-
223-
projectionFactory.setBeanFactory(applicationContext);
224-
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
225220
}
226221

227222
/**
@@ -294,9 +289,11 @@ protected EntityOperations getEntityOperations() {
294289
* projections.
295290
* @see org.springframework.data.projection.SpelAwareProxyProjectionFactory
296291
* @since 2.1
292+
* @deprecated since 3.4, use {@link CassandraConverter#getProjectionFactory()} instead.
297293
*/
294+
@Deprecated
298295
protected SpelAwareProxyProjectionFactory getProjectionFactory() {
299-
return this.projectionFactory;
296+
return (SpelAwareProxyProjectionFactory) getConverter().getProjectionFactory();
300297
}
301298

302299
private CassandraPersistentEntity<?> getRequiredPersistentEntity(Class<?> entityType) {
@@ -365,7 +362,8 @@ public <T> Flux<T> select(Statement<?> statement, Class<T> entityClass) {
365362
Assert.notNull(statement, "Statement must not be null");
366363
Assert.notNull(entityClass, "Entity type must not be null");
367364

368-
Function<Row, T> mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
365+
Function<Row, T> mapper = getMapper(EntityProjection.nonProjecting(entityClass),
366+
EntityQueryUtils.getTableName(statement));
369367

370368
return doQuery(statement, (row, rowNum) -> mapper.apply(row));
371369
}
@@ -421,15 +419,15 @@ public <T> Flux<T> select(Query query, Class<T> entityClass) throws DataAccessEx
421419
<T> Flux<T> doSelect(Query query, Class<?> entityClass, CqlIdentifier tableName, Class<T> returnType) {
422420

423421
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entityClass);
424-
425-
Columns columns = getStatementFactory().computeColumnsForProjection(query.getColumns(), persistentEntity,
422+
EntityProjection<T, ?> projection = entityOperations.introspectProjection(returnType, entityClass);
423+
Columns columns = getStatementFactory().computeColumnsForProjection(projection, query.getColumns(),
424+
persistentEntity,
426425
returnType);
427426

428427
Query queryToUse = query.columns(columns);
429428

430429
StatementBuilder<Select> select = getStatementFactory().select(queryToUse, persistentEntity, tableName);
431-
432-
Function<Row, T> mapper = getMapper(entityClass, returnType, tableName);
430+
Function<Row, T> mapper = getMapper(projection, tableName);
433431

434432
return doQuery(select.build(), (row, rowNum) -> mapper.apply(row));
435433
}
@@ -956,17 +954,15 @@ public String getCql() {
956954
}
957955

958956
@SuppressWarnings("unchecked")
959-
private <T> Function<Row, T> getMapper(Class<?> entityType, Class<T> targetType, CqlIdentifier tableName) {
957+
private <T> Function<Row, T> getMapper(EntityProjection<T, ?> projection, CqlIdentifier tableName) {
960958

961-
Class<?> typeToRead = resolveTypeToRead(entityType, targetType);
959+
Class<T> targetType = projection.getMappedType().getType();
962960

963961
return row -> {
964962

965963
maybeEmitEvent(new AfterLoadEvent<>(row, targetType, tableName));
966964

967-
Object source = getConverter().read(typeToRead, row);
968-
969-
T result = (T) (targetType.isInterface() ? getProjectionFactory().createProjection(targetType, source) : source);
965+
T result = getConverter().project(projection, row);
970966

971967
if (result != null) {
972968
maybeEmitEvent(new AfterConvertEvent<>(row, result, tableName));
@@ -981,10 +977,6 @@ static Mono<WriteResult> toWriteResult(ReactiveResultSet resultSet) {
981977
.map(rows -> new WriteResult(resultSet.getAllExecutionInfo(), resultSet.wasApplied(), rows));
982978
}
983979

984-
private Class<?> resolveTypeToRead(Class<?> entityType, Class<?> targetType) {
985-
return targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType;
986-
}
987-
988980
private static MappingCassandraConverter newConverter(ReactiveSession session) {
989981

990982
MappingCassandraConverter converter = new MappingCassandraConverter();

0 commit comments

Comments
 (0)