15
15
*/
16
16
package org .springframework .data .cassandra .core ;
17
17
18
- import java .beans .PropertyDescriptor ;
19
18
import java .util .ArrayList ;
20
19
import java .util .Collection ;
21
20
import java .util .Collections ;
64
63
import org .springframework .data .cassandra .core .query .VectorSort ;
65
64
import org .springframework .data .convert .EntityWriter ;
66
65
import org .springframework .data .domain .Sort ;
66
+ import org .springframework .data .mapping .context .MappingContext ;
67
67
import org .springframework .data .projection .EntityProjection ;
68
68
import org .springframework .data .projection .EntityProjectionIntrospector ;
69
- import org .springframework .data .projection .ProjectionInformation ;
70
69
import org .springframework .data .util .Predicates ;
71
70
import org .springframework .data .util .ProxyUtils ;
72
71
import org .springframework .util .Assert ;
@@ -123,6 +122,8 @@ public class StatementFactory {
123
122
124
123
private KeyspaceProvider keyspaceProvider = KeyspaceProviders .ENTITY_KEYSPACE ;
125
124
125
+ private ProjectionFunction projectionFunction = ProjectionFunction .projecting ();
126
+
126
127
/**
127
128
* Create {@link StatementFactory} given {@link CassandraConverter}.
128
129
*
@@ -241,6 +242,28 @@ public void setKeyspaceProvider(KeyspaceProvider keyspaceProvider) {
241
242
this .keyspaceProvider = keyspaceProvider ;
242
243
}
243
244
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
+
244
267
/**
245
268
* Create a {@literal COUNT} statement by mapping {@link Query} to {@link Select}.
246
269
*
@@ -287,7 +310,21 @@ public StatementBuilder<Select> count(Query query, CassandraPersistentEntity<?>
287
310
*/
288
311
public StatementBuilder <Select > selectExists (Query query , EntityProjection <?, ?> projection ,
289
312
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 ());
291
328
}
292
329
293
330
/**
@@ -301,10 +338,25 @@ public StatementBuilder<Select> selectExists(Query query, EntityProjection<?, ?>
301
338
*/
302
339
public StatementBuilder <Select > selectOneById (Object id , CassandraPersistentEntity <?> entity ,
303
340
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 ) {
304
355
305
356
Where where = new Where ();
306
357
307
- Columns columns = computeColumnsForProjection (getEntityProjection (entity .getType ()), Columns .empty (), entity );
358
+ Columns columns = computeColumnsForProjection (getEntityProjection (entity .getType ()), Columns .empty (),
359
+ projectionFunction );
308
360
List <Selector > selectors = getQueryMapper ().getMappedSelectors (columns , entity );
309
361
310
362
cassandraConverter .write (id , where , entity );
@@ -341,7 +393,7 @@ public StatementBuilder<Select> select(Query query, CassandraPersistentEntity<?>
341
393
* @since 2.1
342
394
*/
343
395
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 () );
345
397
}
346
398
347
399
/**
@@ -356,12 +408,18 @@ public StatementBuilder<Select> select(Query query, CassandraPersistentEntity<?>
356
408
*/
357
409
public StatementBuilder <Select > select (Query query , EntityProjection <?, ?> projection ,
358
410
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 ) {
359
416
360
417
Assert .notNull (query , "Query must not be null" );
361
418
Assert .notNull (entity , "CassandraPersistentEntity must not be null" );
362
419
Assert .notNull (tableName , "Table name must not be null" );
420
+ Assert .notNull (projectionFunction , "ProjectionFunction must not be null" );
363
421
364
- Columns columns = computeColumnsForProjection (projection , query .getColumns (), entity );
422
+ Columns columns = computeColumnsForProjection (projection , query .getColumns (), projectionFunction );
365
423
366
424
return doSelect (query .columns (columns ), entity , tableName );
367
425
}
@@ -703,38 +761,12 @@ public StatementBuilder<Delete> delete(Object entity, QueryOptions options, Enti
703
761
* @param projectionFunction must not be {@literal null}.
704
762
* @return {@link Columns} with columns to be included.
705
763
*/
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 ) {
732
766
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 ());
738
770
}
739
771
740
772
return columns ;
@@ -1292,6 +1324,7 @@ static class SimpleSelector implements com.datastax.oss.driver.api.querybuilder.
1292
1324
this .selector = selector ;
1293
1325
}
1294
1326
1327
+
1295
1328
@ Override
1296
1329
public com .datastax .oss .driver .api .querybuilder .select .Selector as (CqlIdentifier alias ) {
1297
1330
throw new UnsupportedOperationException ();
@@ -1413,4 +1446,191 @@ enum KeyspaceProviders implements KeyspaceProvider {
1413
1446
1414
1447
}
1415
1448
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
+
1416
1636
}
0 commit comments