Use prepare(String) for Prepared Statement preparation to prevent bind values from being cached.

We now also apply query options from the initial statement to the bound statement by copying these if query options are set.

Closes #1213
This commit is contained in:
Mark Paluch
2022-03-04 14:57:28 +01:00
parent e8d3da2d6f
commit f7edf29d19
5 changed files with 184 additions and 12 deletions

View File

@@ -981,7 +981,7 @@ public class AsyncCassandraTemplate
* @return
*/
protected CompletionStage<PreparedStatement> doPrepare(CqlSession session) {
return session.prepareAsync(statement);
return session.prepareAsync(statement.getQuery());
}
@Override

View File

@@ -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

View File

@@ -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<CqlIdentifier, Object> 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<CqlIdentifier, Object> namedValues = source.getNamedValues();
ColumnDefinitions variableDefinitions = ps.getVariableDefinitions();
CodecRegistry codecRegistry = builder.codecRegistry();
for (Map.Entry<CqlIdentifier, Object> 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 <T> the source type
* @param supplier the value supplier
* @return a {@link Source} that can be used to complete the mapping
* @see #from(Object)
*/
public <T> Source<T> from(Supplier<T> 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 <T> the source type
* @param value the value
* @return a {@link Source} that can be used to complete the mapping
*/
public <T> Source<T> from(@Nullable T value) {
return from(() -> value);
}
private <T> Source<T> getSource(Supplier<T> supplier) {
return new Source<>(SingletonSupplier.of(supplier), t -> true);
}
}
/**
* A source value/supplier that is in the process of being mapped.
*
* @param <T> the source type
*/
static class Source<T> {
private final Supplier<T> supplier;
private final Predicate<T> predicate;
private Source(Supplier<T> supplier, Predicate<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> whenNot(Predicate<T> 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<T> when(Predicate<T> 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<T> consumer) {
Assert.notNull(consumer, "Consumer must not be null");
T value = this.supplier.get();
if (this.predicate.test(value)) {
consumer.accept(value);
}
}
}
}

View File

@@ -957,7 +957,7 @@ public class ReactiveCassandraTemplate
@Override
public Mono<PreparedStatement> createPreparedStatement(ReactiveSession session) throws DriverException {
return session.prepare(statement);
return session.prepare(statement.getQuery());
}
@Override

View File

@@ -181,4 +181,5 @@ public abstract class QueryOptionsUtil {
private static boolean hasTtl(Duration ttl) {
return !ttl.isZero() && !ttl.isNegative();
}
}