diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java index 0208aed7d..e9c72eb82 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java @@ -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 + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index d9a3a77e7..d0f911416 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -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; + }); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java index 48d2df422..2a4426e23 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java @@ -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)); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java index 1da48fef3..3c87e04fd 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java @@ -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 { - @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 { + } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 97cb3e639..b93b46054 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -12,6 +12,7 @@ This chapter summarizes changes and new features for each release. * Filter conditions for lightweight transaction update and delete (`UPDATE … IF `, `DELETE … IF `). * 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