DATACASS-454 - Extend @Query to declare idempotent CQL queries.
String-based queries can now be marked as idempotent. Also, SELECT queries defined via @Query are also considered idempotent to make these retryable.
interface PersonRepository extends Repository<Person, String> {
@Query("SELECT * FROM person WHERE lastname = ?0;") // idempotent by default
Person findByLastname(String lastname);
@Query(value = "UPDATE person SET lastname = ?0", idempotent = Query.Idempotency.IDEMPOTENT)
void updatePerson(String name);
}
This commit is contained in:
@@ -49,6 +49,14 @@ public @interface Query {
|
||||
*/
|
||||
boolean allowFiltering() default false;
|
||||
|
||||
/**
|
||||
* Specifies whether the {@link #value() CQL query} is {@link com.datastax.driver.core.Statement#isIdempotent}.
|
||||
* {@code SELECT} statements are considered {@link Idempotency#IDEMPOTENT idempotent} by default.
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
Idempotency idempotent() default Idempotency.UNDEFINED;
|
||||
|
||||
/**
|
||||
* Returns whether the defined query should be executed as a count projection.
|
||||
*
|
||||
@@ -63,4 +71,28 @@ public @interface Query {
|
||||
*/
|
||||
boolean exists() default false;
|
||||
|
||||
/**
|
||||
* Enumeration to define statement idempotency.
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
enum Idempotency {
|
||||
|
||||
/**
|
||||
* Undefined state (default for all non-{@code SELECT} statements. Leaves
|
||||
* {@link com.datastax.driver.core.Statement#setIdempotent(boolean)} state unchanged.
|
||||
*/
|
||||
UNDEFINED,
|
||||
|
||||
/**
|
||||
* Statement considered idempotent.
|
||||
*/
|
||||
IDEMPOTENT,
|
||||
|
||||
/**
|
||||
* Statement considered non-idempotent. Sets {@link com.datastax.driver.core.Statement#setIdempotent(boolean)} to
|
||||
* {@code false}.
|
||||
*/
|
||||
NON_IDEMPOTENT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
@@ -24,6 +25,7 @@ import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
|
||||
import org.springframework.data.cassandra.repository.Consistency;
|
||||
import org.springframework.data.cassandra.repository.Query;
|
||||
import org.springframework.data.cassandra.repository.Query.Idempotency;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
@@ -214,7 +216,7 @@ public class CassandraQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true is the method returns a {@link ResultSet}.
|
||||
* @return {@literal true} if the method returns a {@link ResultSet}.
|
||||
*/
|
||||
public boolean isResultSetQuery() {
|
||||
|
||||
@@ -222,4 +224,24 @@ public class CassandraQueryMethod extends QueryMethod {
|
||||
|
||||
return actualType != null && ResultSet.class.isAssignableFrom(actualType.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Query {@link Idempotency}. Defaults to {@link Idempotency#IDEMPOTENT} for {@code SELECT} queries.
|
||||
*/
|
||||
Idempotency getIdempotency() {
|
||||
return this.query.filter(it -> it.idempotent() != Idempotency.UNDEFINED) //
|
||||
.map(Query::idempotent) //
|
||||
.orElseGet(() -> {
|
||||
|
||||
String cql = getAnnotatedQuery();
|
||||
if (StringUtils.hasText(cql)) {
|
||||
|
||||
if (cql.trim().toUpperCase(Locale.ENGLISH).startsWith("SELECT ")) {
|
||||
return Idempotency.IDEMPOTENT;
|
||||
}
|
||||
}
|
||||
|
||||
return Idempotency.UNDEFINED;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
|
||||
import org.springframework.data.cassandra.core.query.Columns;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
import org.springframework.data.cassandra.repository.Query.Idempotency;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.repository.query.QueryCreationException;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
@@ -213,6 +214,11 @@ class QueryStatementCreator {
|
||||
queryToUse.setConsistencyLevel(this.queryMethod.getRequiredAnnotatedConsistencyLevel());
|
||||
}
|
||||
|
||||
Idempotency idempotency = this.queryMethod.getIdempotency();
|
||||
if (idempotency != Idempotency.UNDEFINED) {
|
||||
queryToUse.setIdempotent(idempotency == Idempotency.IDEMPOTENT);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Created query [%s].", queryToUse));
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -82,7 +82,6 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
private ProjectionFactory factory;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
|
||||
CassandraMappingContext mappingContext = new CassandraMappingContext();
|
||||
@@ -137,7 +136,7 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
assertThat(actual.getObject(0)).isEqualTo("Mat\th'ew\"s");
|
||||
}
|
||||
|
||||
@Test // DATACASS-117
|
||||
@Test // DATACASS-117, DATACASS-454
|
||||
public void bindsAndEscapesBytesIndexParameterCorrectly() {
|
||||
|
||||
StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class);
|
||||
@@ -146,10 +145,35 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
|
||||
SimpleStatement actual = cassandraQuery.createQuery(accessor);
|
||||
|
||||
assertThat(actual.isIdempotent()).isTrue();
|
||||
assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;");
|
||||
assertThat(actual.getObject(0)).isEqualTo(ByteBuffer.wrap(new byte[] { 1, 2, 3, 4 }));
|
||||
}
|
||||
|
||||
@Test // DATACASS-454
|
||||
public void shouldConsiderNonIdempotentOverride() {
|
||||
|
||||
StringBasedCassandraQuery cassandraQuery = getQueryMethod("nonIdempotentSelect", String.class);
|
||||
CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(
|
||||
cassandraQuery.getQueryMethod(), "Matthews");
|
||||
|
||||
SimpleStatement actual = cassandraQuery.createQuery(accessor);
|
||||
|
||||
assertThat(actual.isIdempotent()).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACASS-454
|
||||
public void shouldNotApplyIdempotencyToNonSelectStatement() {
|
||||
|
||||
StringBasedCassandraQuery cassandraQuery = getQueryMethod("nonIdempotentDelete");
|
||||
CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(
|
||||
cassandraQuery.getQueryMethod());
|
||||
|
||||
SimpleStatement actual = cassandraQuery.createQuery(accessor);
|
||||
|
||||
assertThat(actual.isIdempotent()).isNull();
|
||||
}
|
||||
|
||||
@Test // DATACASS-117
|
||||
public void bindsIndexParameterInListCorrectly() {
|
||||
|
||||
@@ -349,8 +373,8 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
|
||||
StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", QueryOptions.class, String.class);
|
||||
|
||||
CassandraParametersParameterAccessor parameterAccessor =
|
||||
new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), queryOptions, "Matthews");
|
||||
CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor(
|
||||
cassandraQuery.getQueryMethod(), queryOptions, "Matthews");
|
||||
|
||||
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor);
|
||||
|
||||
@@ -364,8 +388,8 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
|
||||
StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class);
|
||||
|
||||
CassandraParametersParameterAccessor parameterAccessor =
|
||||
new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), "Matthews");
|
||||
CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor(
|
||||
cassandraQuery.getQueryMethod(), "Matthews");
|
||||
|
||||
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor);
|
||||
|
||||
@@ -378,8 +402,8 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
|
||||
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
|
||||
|
||||
CassandraQueryMethod queryMethod =
|
||||
new CassandraQueryMethod(method, metadata, factory, converter.getMappingContext());
|
||||
CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory,
|
||||
converter.getMappingContext());
|
||||
|
||||
return new StringBasedCassandraQuery(queryMethod, operations, PARSER,
|
||||
ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
|
||||
@@ -388,10 +412,18 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
@SuppressWarnings("unused")
|
||||
private interface SampleRepository extends Repository<Person, String> {
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = ?0;")
|
||||
@Query(value = "SELECT * FROM person WHERE lastname = ?0;")
|
||||
@Consistency(ConsistencyLevel.LOCAL_ONE)
|
||||
Person findByLastname(String lastname);
|
||||
|
||||
@Query(value = "SELECT * FROM person WHERE lastname = ?0;", idempotent = Query.Idempotency.NON_IDEMPOTENT)
|
||||
@Consistency(ConsistencyLevel.LOCAL_ONE)
|
||||
Person nonIdempotentSelect(String lastname);
|
||||
|
||||
@Query(value = "DELETE FROM person")
|
||||
@Consistency(ConsistencyLevel.LOCAL_ONE)
|
||||
Person nonIdempotentDelete();
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = ?0;")
|
||||
Person findByLastname(QueryOptions queryOptions, String lastname);
|
||||
|
||||
@@ -441,6 +473,7 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Query("SELECT * FROM person WHERE lastname = ?0;")
|
||||
@interface ComposedQueryAnnotation { }
|
||||
@interface ComposedQueryAnnotation {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ This chapter summarizes changes and new features for each release.
|
||||
* Filter conditions for lightweight transaction update and delete (`UPDATE … IF <condition>`, `DELETE … IF <condition>`).
|
||||
* Optimistic Locking support.
|
||||
* Auditing via `@EnableCassandraAuditing`.
|
||||
* Idempotency support in `@Query` annotation.
|
||||
|
||||
[[new-features.2-1-0]]
|
||||
== What's new in Spring Data for Apache Cassandra 2.1
|
||||
|
||||
Reference in New Issue
Block a user