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:
Mark Paluch
2019-06-18 15:03:24 +02:00
parent 8064a6d1dd
commit 4f93fb830b
5 changed files with 107 additions and 13 deletions

View File

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

View File

@@ -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;
});
}
}

View File

@@ -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));
}

View File

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

View File

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