1515 */
1616package org .springframework .data .cassandra .core ;
1717
18- import java .beans .PropertyDescriptor ;
1918import java .util .ArrayList ;
2019import java .util .Collection ;
2120import java .util .Collections ;
6463import org .springframework .data .cassandra .core .query .VectorSort ;
6564import org .springframework .data .convert .EntityWriter ;
6665import org .springframework .data .domain .Sort ;
66+ import org .springframework .data .mapping .context .MappingContext ;
6767import org .springframework .data .projection .EntityProjection ;
6868import org .springframework .data .projection .EntityProjectionIntrospector ;
69- import org .springframework .data .projection .ProjectionInformation ;
7069import org .springframework .data .util .Predicates ;
7170import org .springframework .data .util .ProxyUtils ;
7271import 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