diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java index cfb374bcb..f5036e38a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java @@ -981,7 +981,7 @@ public class AsyncCassandraTemplate * @return */ protected CompletionStage doPrepare(CqlSession session) { - return session.prepareAsync(statement); + return session.prepareAsync(statement.getQuery()); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index b6777eee4..36a91ec67 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -972,7 +972,7 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP @Override public PreparedStatement createPreparedStatement(CqlSession session) throws DriverException { - return session.prepare(statement); + return session.prepare(statement.getQuery()); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/PreparedStatementDelegate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/PreparedStatementDelegate.java index 7952b2a7f..7ccc5abf3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/PreparedStatementDelegate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/PreparedStatementDelegate.java @@ -16,11 +16,18 @@ package org.springframework.data.cassandra.core; import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.springframework.data.cassandra.core.cql.QueryExtractorDelegate; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.cql.BoundStatement; @@ -30,6 +37,7 @@ import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.core.cql.Statement; import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; /** * Support class for Cassandra Template API implementation classes that want to make use of prepared statements. @@ -40,30 +48,51 @@ import com.datastax.oss.driver.api.core.type.DataType; class PreparedStatementDelegate { /** - * Bind values held in {@link SimpleStatement} to the {@link PreparedStatement}. + * Bind values held in {@link SimpleStatement} to the {@link PreparedStatement} and apply query options that are set + * or do not match the default value. * - * @param statement + * @param source * @param ps * @return the bound statement. */ - static BoundStatement bind(SimpleStatement statement, PreparedStatement ps) { + static BoundStatement bind(SimpleStatement source, PreparedStatement ps) { - BoundStatementBuilder boundStatementBuilder = ps.boundStatementBuilder(statement.getPositionalValues().toArray()); - Map namedValues = statement.getNamedValues(); + BoundStatementBuilder builder = ps.boundStatementBuilder(source.getPositionalValues().toArray()); + + Mapper mapper = Mapper.INSTANCE; + + mapper.from(source.getExecutionProfileName()).whenHasText().to(builder::setExecutionProfileName); + mapper.from(source.getExecutionProfile()).whenNonNull().to(builder::setExecutionProfile); + mapper.from(source.getRoutingKeyspace()).whenNonNull().to(builder::setRoutingKeyspace); + mapper.from(source.getRoutingKey()).whenNonNull().to(builder::setRoutingKey); + mapper.from(source.getRoutingToken()).whenNonNull().to(builder::setRoutingToken); + mapper.from(source.isIdempotent()).whenNonNull().to(builder::setIdempotence); + mapper.from(source.isTracing()).whenNonNull().to(builder::setTracing); + mapper.from(source.getQueryTimestamp()).whenNot(it -> it == Statement.NO_DEFAULT_TIMESTAMP) + .to(builder::setQueryTimestamp); + mapper.from(source.getPagingState()).whenNonNull().to(builder::setPagingState); + mapper.from(source.getPageSize()).whenNot(it -> it == 0L).to(builder::setPageSize); + mapper.from(source.getConsistencyLevel()).whenNonNull().to(builder::setConsistencyLevel); + mapper.from(source.getSerialConsistencyLevel()).whenNonNull().to(builder::setSerialConsistencyLevel); + mapper.from(source.getTimeout()).whenNonNull().to(builder::setTimeout); + mapper.from(source.getNode()).whenNonNull().to(builder::setNode); + mapper.from(source.getNowInSeconds()).whenNot(it -> it == Statement.NO_NOW_IN_SECONDS).to(builder::setNowInSeconds); + + Map namedValues = source.getNamedValues(); ColumnDefinitions variableDefinitions = ps.getVariableDefinitions(); + CodecRegistry codecRegistry = builder.codecRegistry(); for (Map.Entry entry : namedValues.entrySet()) { if (entry.getValue() == null) { - boundStatementBuilder = boundStatementBuilder.setToNull(entry.getKey()); + builder = builder.setToNull(entry.getKey()); } else { DataType type = variableDefinitions.get(entry.getKey()).getType(); - boundStatementBuilder = boundStatementBuilder.set(entry.getKey(), entry.getValue(), - boundStatementBuilder.codecRegistry().codecFor(type)); + builder = builder.set(entry.getKey(), entry.getValue(), codecRegistry.codecFor(type)); } } - return ps.bind(statement.getPositionalValues().toArray()); + return builder.build(); } /** @@ -117,4 +146,146 @@ class PreparedStatementDelegate { return String.format("Cannot prepare statement %s. Statement must be a SimpleStatement.", statement); } + enum Mapper { + + INSTANCE; + + /** + * Return a new {@link Source} from the specified value supplier that can be used to perform the mapping. + * + * @param the source type + * @param supplier the value supplier + * @return a {@link Source} that can be used to complete the mapping + * @see #from(Object) + */ + public Source from(Supplier supplier) { + + Assert.notNull(supplier, "Supplier must not be null"); + return getSource(supplier); + } + + /** + * Return a new {@link Source} from the specified value that can be used to perform the mapping. + * + * @param the source type + * @param value the value + * @return a {@link Source} that can be used to complete the mapping + */ + public Source from(@Nullable T value) { + return from(() -> value); + } + + private Source getSource(Supplier supplier) { + return new Source<>(SingletonSupplier.of(supplier), t -> true); + } + } + + /** + * A source value/supplier that is in the process of being mapped. + * + * @param the source type + */ + static class Source { + + private final Supplier supplier; + + private final Predicate predicate; + + private Source(Supplier supplier, Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + + this.supplier = supplier; + this.predicate = predicate; + } + + /** + * Return a filtered version of the source that won't map non-null values or suppliers that throw a + * {@link NullPointerException}. + * + * @return a new filtered source instance + */ + public Source whenNonNull() { + return new Source<>(this.supplier, Objects::nonNull); + } + + /** + * Return a filtered version of the source that will only map values that are {@code true}. + * + * @return a new filtered source instance + */ + public Source whenTrue() { + return when(Boolean.TRUE::equals); + } + + /** + * Return a filtered version of the source that will only map values that are {@code false}. + * + * @return a new filtered source instance + */ + public Source whenFalse() { + return when(Boolean.FALSE::equals); + } + + /** + * Return a filtered version of the source that will only map values that have a {@code toString()} containing + * actual text. + * + * @return a new filtered source instance + */ + public Source whenHasText() { + return when((value) -> StringUtils.hasText(Objects.toString(value, null))); + } + + /** + * Return a filtered version of the source that will only map values equal to the specified {@code object}. + * + * @param object the object to match + * @return a new filtered source instance + */ + public Source whenEqualTo(Object object) { + return when(object::equals); + } + + /** + * Return a filtered version of the source that won't map values that match the given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source whenNot(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + return when(predicate.negate()); + } + + /** + * Return a filtered version of the source that won't map values that don't match the given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source when(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + return new Source<>(this.supplier, (this.predicate != null) ? this.predicate.and(predicate) : predicate); + } + + /** + * Complete the mapping by passing any non-filtered value to the specified consumer. + * + * @param consumer the consumer that should accept the value if it's not been filtered + */ + public void to(Consumer consumer) { + + Assert.notNull(consumer, "Consumer must not be null"); + + T value = this.supplier.get(); + if (this.predicate.test(value)) { + consumer.accept(value); + } + } + + } + } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java index 7a63cf350..686c6992f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java @@ -957,7 +957,7 @@ public class ReactiveCassandraTemplate @Override public Mono createPreparedStatement(ReactiveSession session) throws DriverException { - return session.prepare(statement); + return session.prepare(statement.getQuery()); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/QueryOptionsUtil.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/QueryOptionsUtil.java index 45ce50ec5..6446ce975 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/QueryOptionsUtil.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/QueryOptionsUtil.java @@ -181,4 +181,5 @@ public abstract class QueryOptionsUtil { private static boolean hasTtl(Duration ttl) { return !ttl.isZero() && !ttl.isNegative(); } + }