Skip to content

Commit 44a9bc8

Browse files
committed
Add support ProjectionFunction to determine default selection projection.
We now support projection functions that can be configured on the StatementFactory to determine the default Columns projection. DTO/Interface projections are also now computed through ProjectionFunction that is composable for chaining fallback options. Closes #1590
1 parent 46d502a commit 44a9bc8

File tree

7 files changed

+315
-49
lines changed

7 files changed

+315
-49
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,17 +339,17 @@ public TerminalSelectExists<T> matching(Query query) {
339339
@Override
340340
public TerminalSelectExists<T> matchingId(Object identifier) {
341341

342-
SimpleStatement statement = statementFactory.selectOneById(identifier, persistentEntity, tableName).build();
343-
344342
return new TerminalSelectExists<>() {
345343

346344
@Override
347345
public <V> V exists(Function<Statement<?>, V> function) {
346+
SimpleStatement statement = statementFactory.selectExists(identifier, persistentEntity, tableName).build();
348347
return function.apply(statement);
349348
}
350349

351350
@Override
352351
public <V> V select(BiFunction<Statement<?>, RowMapper<T>, V> function) {
352+
SimpleStatement statement = statementFactory.selectOneById(identifier, persistentEntity, tableName).build();
353353
RowMapper<T> rowMapper = getRowMapper(entityClass, tableName, QueryResultConverter.entity());
354354
return function.apply(statement, rowMapper);
355355
}

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

Lines changed: 257 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.springframework.data.cassandra.core;
1717

18-
import java.beans.PropertyDescriptor;
1918
import java.util.ArrayList;
2019
import java.util.Collection;
2120
import java.util.Collections;
@@ -64,9 +63,9 @@
6463
import org.springframework.data.cassandra.core.query.VectorSort;
6564
import org.springframework.data.convert.EntityWriter;
6665
import org.springframework.data.domain.Sort;
66+
import org.springframework.data.mapping.context.MappingContext;
6767
import org.springframework.data.projection.EntityProjection;
6868
import org.springframework.data.projection.EntityProjectionIntrospector;
69-
import org.springframework.data.projection.ProjectionInformation;
7069
import org.springframework.data.util.Predicates;
7170
import org.springframework.data.util.ProxyUtils;
7271
import org.springframework.util.Assert;
@@ -123,6 +122,8 @@ public class StatementFactory {
123122

124123
private KeyspaceProvider keyspaceProvider = KeyspaceProviders.ENTITY_KEYSPACE;
125124

125+
private ProjectionFunction projectionFunction = ProjectionFunction.projecting();
126+
126127
/**
127128
* Create {@link StatementFactory} given {@link CassandraConverter}.
128129
*
@@ -241,6 +242,28 @@ public void setKeyspaceProvider(KeyspaceProvider keyspaceProvider) {
241242
this.keyspaceProvider = keyspaceProvider;
242243
}
243244

245+
/**
246+
* @return the configured projection function.
247+
* @since 5.0
248+
*/
249+
public ProjectionFunction getProjectionFunction() {
250+
return projectionFunction;
251+
}
252+
253+
/**
254+
* Set the default {@link ProjectionFunction} to determine {@link Columns} for a {@link Select} statement if the query
255+
* did not specify any columns.
256+
*
257+
* @param projectionFunction the projection function to use, must not be {@literal null}.
258+
* @since 5.0
259+
*/
260+
public void setProjectionFunction(ProjectionFunction projectionFunction) {
261+
262+
Assert.notNull(projectionFunction, "ProjectionFunction must not be null");
263+
264+
this.projectionFunction = projectionFunction;
265+
}
266+
244267
/**
245268
* Create a {@literal COUNT} statement by mapping {@link Query} to {@link Select}.
246269
*
@@ -287,7 +310,21 @@ public StatementBuilder<Select> count(Query query, CassandraPersistentEntity<?>
287310
*/
288311
public StatementBuilder<Select> selectExists(Query query, EntityProjection<?, ?> projection,
289312
CassandraPersistentEntity<?> entity, CqlIdentifier tableName) {
290-
return select(query.limit(1), projection, entity, tableName);
313+
return select(query.limit(1), projection, entity, tableName, ProjectionFunction.primaryKey());
314+
}
315+
316+
/**
317+
* Create an {@literal SELECT} statement by mapping {@code id} to {@literal SELECT … WHERE}. This method supports
318+
* composite primary keys as part of the entity class itself or as separate primary key class.
319+
*
320+
* @param id must not be {@literal null}.
321+
* @param entity must not be {@literal null}.
322+
* @param tableName must not be {@literal null}.
323+
* @return the select builder.
324+
*/
325+
public StatementBuilder<Select> selectExists(Object id, CassandraPersistentEntity<?> entity,
326+
CqlIdentifier tableName) {
327+
return selectOneById(id, entity, tableName, ProjectionFunction.primaryKey());
291328
}
292329

293330
/**
@@ -301,10 +338,25 @@ public StatementBuilder<Select> selectExists(Query query, EntityProjection<?, ?>
301338
*/
302339
public StatementBuilder<Select> selectOneById(Object id, CassandraPersistentEntity<?> entity,
303340
CqlIdentifier tableName) {
341+
return selectOneById(id, entity, tableName, ProjectionFunction.empty());
342+
}
343+
344+
/**
345+
* Create an {@literal SELECT} statement by mapping {@code id} to {@literal SELECT … WHERE}. This method supports
346+
* composite primary keys as part of the entity class itself or as separate primary key class.
347+
*
348+
* @param id must not be {@literal null}.
349+
* @param entity must not be {@literal null}.
350+
* @param tableName must not be {@literal null}.
351+
* @return the select builder.
352+
*/
353+
private StatementBuilder<Select> selectOneById(Object id, CassandraPersistentEntity<?> entity,
354+
CqlIdentifier tableName, ProjectionFunction projectionFunction) {
304355

305356
Where where = new Where();
306357

307-
Columns columns = computeColumnsForProjection(getEntityProjection(entity.getType()), Columns.empty(), entity);
358+
Columns columns = computeColumnsForProjection(getEntityProjection(entity.getType()), Columns.empty(),
359+
projectionFunction);
308360
List<Selector> selectors = getQueryMapper().getMappedSelectors(columns, entity);
309361

310362
cassandraConverter.write(id, where, entity);
@@ -341,7 +393,7 @@ public StatementBuilder<Select> select(Query query, CassandraPersistentEntity<?>
341393
* @since 2.1
342394
*/
343395
public StatementBuilder<Select> select(Query query, CassandraPersistentEntity<?> entity, CqlIdentifier tableName) {
344-
return select(query, getEntityProjection(entity.getType()), entity, tableName);
396+
return select(query, getEntityProjection(entity.getType()), entity, tableName, ProjectionFunction.empty());
345397
}
346398

347399
/**
@@ -356,12 +408,18 @@ public StatementBuilder<Select> select(Query query, CassandraPersistentEntity<?>
356408
*/
357409
public StatementBuilder<Select> select(Query query, EntityProjection<?, ?> projection,
358410
CassandraPersistentEntity<?> entity, CqlIdentifier tableName) {
411+
return select(query, projection, entity, tableName, ProjectionFunction.empty());
412+
}
413+
414+
private StatementBuilder<Select> select(Query query, EntityProjection<?, ?> projection,
415+
CassandraPersistentEntity<?> entity, CqlIdentifier tableName, ProjectionFunction projectionFunction) {
359416

360417
Assert.notNull(query, "Query must not be null");
361418
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
362419
Assert.notNull(tableName, "Table name must not be null");
420+
Assert.notNull(projectionFunction, "ProjectionFunction must not be null");
363421

364-
Columns columns = computeColumnsForProjection(projection, query.getColumns(), entity);
422+
Columns columns = computeColumnsForProjection(projection, query.getColumns(), projectionFunction);
365423

366424
return doSelect(query.columns(columns), entity, tableName);
367425
}
@@ -703,38 +761,12 @@ public StatementBuilder<Delete> delete(Object entity, QueryOptions options, Enti
703761
* @param projectionFunction must not be {@literal null}.
704762
* @return {@link Columns} with columns to be included.
705763
*/
706-
@SuppressWarnings("NullAway")
707-
Columns computeColumnsForProjection(EntityProjection<?, ?> projection, Columns columns,
708-
CassandraPersistentEntity<?> domainType) {
709-
710-
Class<?> returnType = projection.getMappedType().getType();
711-
if (!columns.isEmpty()
712-
|| ClassUtils.isAssignable(projection.getActualDomainType().getType(), projection.getMappedType().getType())
713-
|| ClassUtils.isAssignable(Map.class, returnType) || ClassUtils.isAssignable(ResultSet.class, returnType)) {
714-
return columns;
715-
}
716-
717-
if (projection.getMappedType().getType().isInterface()) {
718-
ProjectionInformation projectionInformation = cassandraConverter.getProjectionFactory()
719-
.getProjectionInformation(projection.getMappedType().getType());
720-
721-
if (projectionInformation.isClosed()) {
722-
723-
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
724-
columns = columns.include(inputProperty.getName());
725-
}
726-
}
727-
} else {
728-
729-
// DTO projections use merged metadata between domain type and result type
730-
PersistentPropertyTranslator translator = PersistentPropertyTranslator.create(domainType,
731-
Predicates.negate(CassandraPersistentProperty::hasExplicitColumnName));
764+
private Columns computeColumnsForProjection(EntityProjection<?, ?> projection, Columns columns,
765+
ProjectionFunction projectionFunction) {
732766

733-
CassandraPersistentEntity<?> entity = getQueryMapper().getConverter().getMappingContext()
734-
.getRequiredPersistentEntity(projection.getMappedType());
735-
for (CassandraPersistentProperty property : entity) {
736-
columns = columns.include(translator.translate(property).getColumnName());
737-
}
767+
if (columns.isEmpty()) {
768+
return projectionFunction.otherwise(getProjectionFunction()).computeProjection(projection,
769+
this.cassandraConverter.getMappingContext());
738770
}
739771

740772
return columns;
@@ -1292,6 +1324,7 @@ static class SimpleSelector implements com.datastax.oss.driver.api.querybuilder.
12921324
this.selector = selector;
12931325
}
12941326

1327+
12951328
@Override
12961329
public com.datastax.oss.driver.api.querybuilder.select.Selector as(CqlIdentifier alias) {
12971330
throw new UnsupportedOperationException();
@@ -1413,4 +1446,191 @@ enum KeyspaceProviders implements KeyspaceProvider {
14131446

14141447
}
14151448

1449+
/**
1450+
* Strategy interface to compute {@link Columns column projection} to be selected for a query based on a given
1451+
* {@link EntityProjection}. A projection function can be composed into a higher-order function using
1452+
* {@link #otherwise(ProjectionFunction)} to form a chain of functions that are tried in sequence until one produces a
1453+
* non-empty {@link Columns} object.
1454+
*
1455+
* @since 5.0
1456+
*/
1457+
public interface ProjectionFunction {
1458+
1459+
/**
1460+
* Compute {@link Columns} to be selected for a given {@link EntityProjection}. If the function cannot compute
1461+
* columns it should return an empty {@link Columns#empty() Columns} object.
1462+
*
1463+
* @param projection the projection to compute columns for.
1464+
* @param context the mapping context.
1465+
* @return the computed {@link Columns} or an empty {@link Columns#empty() Columns} object.
1466+
*/
1467+
Columns computeProjection(EntityProjection<?, ?> projection,
1468+
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> context);
1469+
1470+
/**
1471+
* Compose this projection function with a {@code fallback} function that is invoked when this function returns an
1472+
* empty {@link Columns} object.
1473+
*
1474+
* @param fallback the fallback function.
1475+
* @return the composed ProjectionFunction.
1476+
*/
1477+
default ProjectionFunction otherwise(ProjectionFunction fallback) {
1478+
1479+
return (projection, mappingContext) -> {
1480+
Columns columns = computeProjection(projection, mappingContext);
1481+
return columns.isEmpty() ? fallback.computeProjection(projection, mappingContext) : columns;
1482+
};
1483+
}
1484+
1485+
/**
1486+
* Empty projection function that returns {@link Columns#empty()}.
1487+
*
1488+
* @return a projection function that returns {@link Columns#empty()}.
1489+
*/
1490+
static ProjectionFunction empty() {
1491+
return ProjectionFunctions.EMPTY;
1492+
}
1493+
1494+
/**
1495+
* Projection function that selects the primary key.
1496+
*
1497+
* @return a projection function that selects the primary key.
1498+
*/
1499+
static ProjectionFunction primaryKey() {
1500+
return ProjectionFunctions.PRIMARY_KEY;
1501+
}
1502+
1503+
/**
1504+
* Projection function that selects mapped properties only.
1505+
*
1506+
* @return a projection function that selects mapped properties only.
1507+
*/
1508+
static ProjectionFunction mappedProperties() {
1509+
return ProjectionFunctions.MAPPED_PROPERTIES;
1510+
}
1511+
1512+
/**
1513+
* Projection function that computes columns to be selected for DTO and closed interface projections.
1514+
*
1515+
* @return a projection function that derives columns from DTO and interface projections.
1516+
*/
1517+
static ProjectionFunction projecting() {
1518+
return ProjectionFunctions.PROJECTION;
1519+
}
1520+
1521+
}
1522+
1523+
/**
1524+
* Collection of projection functions.
1525+
*
1526+
* @since 5.0
1527+
*/
1528+
private enum ProjectionFunctions implements ProjectionFunction {
1529+
1530+
/**
1531+
* No-op projection function.
1532+
*/
1533+
EMPTY {
1534+
1535+
@Override
1536+
public Columns computeProjection(EntityProjection<?, ?> projection,
1537+
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> context) {
1538+
return Columns.empty();
1539+
}
1540+
1541+
@Override
1542+
public ProjectionFunction otherwise(ProjectionFunction fallback) {
1543+
return fallback;
1544+
}
1545+
},
1546+
1547+
/**
1548+
* Mapped properties only (no interface/DTO projections).
1549+
*/
1550+
MAPPED_PROPERTIES {
1551+
1552+
@Override
1553+
public Columns computeProjection(EntityProjection<?, ?> projection,
1554+
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> context) {
1555+
1556+
CassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(projection.getActualDomainType());
1557+
1558+
List<String> properties = new ArrayList<>();
1559+
1560+
for (CassandraPersistentProperty property : entity) {
1561+
properties.add(property.getName());
1562+
}
1563+
1564+
return Columns.from(properties.toArray(new String[0]));
1565+
}
1566+
},
1567+
1568+
/**
1569+
* Select the primary key.
1570+
*/
1571+
PRIMARY_KEY {
1572+
1573+
@Override
1574+
public Columns computeProjection(EntityProjection<?, ?> projection,
1575+
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> context) {
1576+
1577+
CassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(projection.getActualDomainType());
1578+
1579+
List<String> primaryKeyColumns = new ArrayList<>();
1580+
1581+
for (CassandraPersistentProperty property : entity) {
1582+
if (property.isIdProperty()) {
1583+
primaryKeyColumns.add(property.getName());
1584+
}
1585+
}
1586+
1587+
return Columns.from(primaryKeyColumns.toArray(new String[0]));
1588+
}
1589+
},
1590+
1591+
/**
1592+
* Compute the projection for DTO and closed interface projections.
1593+
*/
1594+
PROJECTION {
1595+
1596+
@Override
1597+
public Columns computeProjection(EntityProjection<?, ?> projection,
1598+
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> context) {
1599+
1600+
if (!projection.isProjection()) {
1601+
return Columns.empty();
1602+
}
1603+
1604+
if (ClassUtils.isAssignable(projection.getDomainType().getType(), projection.getMappedType().getType())
1605+
|| ClassUtils.isAssignable(Map.class, projection.getMappedType().getType()) //
1606+
|| ClassUtils.isAssignable(ResultSet.class, projection.getMappedType().getType())) {
1607+
return Columns.empty();
1608+
}
1609+
1610+
Columns columns = Columns.empty();
1611+
1612+
if (projection.isClosedProjection()) {
1613+
1614+
for (EntityProjection.PropertyProjection<?, ?> propertyProjection : projection) {
1615+
columns = columns.include(propertyProjection.getPropertyPath().toDotPath());
1616+
}
1617+
} else {
1618+
1619+
CassandraPersistentEntity<?> mapped = context.getRequiredPersistentEntity(projection.getMappedType());
1620+
1621+
// DTO projections use merged metadata between domain type and result type
1622+
PersistentPropertyTranslator translator = PersistentPropertyTranslator.create(
1623+
context.getRequiredPersistentEntity(projection.getActualDomainType()),
1624+
Predicates.negate(CassandraPersistentProperty::hasExplicitColumnName));
1625+
1626+
for (CassandraPersistentProperty property : mapped) {
1627+
columns = columns.include(translator.translate(property).getColumnName());
1628+
}
1629+
}
1630+
1631+
return columns;
1632+
}
1633+
};
1634+
}
1635+
14161636
}

0 commit comments

Comments
 (0)