From f8d87e24bd9bf1e2a8d6ccb26af193ba4d0ab229 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 18 Jun 2019 17:20:41 +0200 Subject: [PATCH] DATACASS-611 - Add query derivation for delete queries. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support query derivation for delete queries using delete…By in method declarations. interface PersonRepository extends Repository { void deleteWithoutResultByLastname(String lastname); boolean deleteByLastname(String lastname); } --- .../query/AbstractCassandraQuery.java | 10 +++++ .../query/AbstractReactiveCassandraQuery.java | 12 ++++++ .../query/PartTreeCassandraQuery.java | 12 ++++++ .../query/QueryStatementCreator.java | 26 +++++++++++++ .../ReactiveCassandraQueryExecution.java | 14 ++++++- .../query/ReactivePartTreeCassandraQuery.java | 12 ++++++ .../ReactiveStringBasedCassandraQuery.java | 8 ++++ .../query/StringBasedCassandraQuery.java | 8 ++++ .../QueryDerivationIntegrationTests.java | 21 ++++++++++ ...veCassandraRepositoryIntegrationTests.java | 38 ++++++++++++++----- .../PartTreeCassandraQueryUnitTests.java | 12 ++++++ ...activePartTreeCassandraQueryUnitTests.java | 11 ++++++ src/main/asciidoc/new-features.adoc | 1 + .../reference/cassandra-repositories.adoc | 20 ++++++++++ 14 files changed, 194 insertions(+), 11 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java index 0184cf84e..d56308de4 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java @@ -146,6 +146,8 @@ public abstract class AbstractCassandraQuery extends CassandraRepositoryQuerySup return ((statement, type) -> new SingleEntityExecution(getOperations(), false).execute(statement, Long.class)); } else if (isExistsQuery()) { return new ExistsExecution(getOperations()); + } else if (isModifyingQuery()) { + return ((statement, type) -> getOperations().getCqlOperations().queryForResultSet(statement).wasApplied()); } else { return new SingleEntityExecution(getOperations(), isLimiting()); } @@ -174,4 +176,12 @@ public abstract class AbstractCassandraQuery extends CassandraRepositoryQuerySup * @since 2.0.4 */ protected abstract boolean isLimiting(); + + /** + * Returns whether the query is a modifying query. + * + * @return a boolean value indicating whether the query is a modifying query. + * @since 2.2 + */ + protected abstract boolean isModifyingQuery(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java index 1d33f7d98..cefa5f0a4 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java @@ -20,6 +20,7 @@ import reactor.core.publisher.Mono; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.ReactiveResultSet; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.convert.CassandraConverter; @@ -151,6 +152,9 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository Long.class)); } else if (isExistsQuery()) { return new ExistsExecution(getReactiveCassandraOperations()); + } else if (isModifyingQuery()) { + return (statement, type) -> getReactiveCassandraOperations().getReactiveCqlOperations() + .queryForResultSet(statement).map(ReactiveResultSet::wasApplied); } else { return new SingleEntityExecution(getReactiveCassandraOperations(), isLimiting()); } @@ -180,6 +184,14 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository */ protected abstract boolean isLimiting(); + /** + * Returns whether the query is a modifying query. + * + * @return a boolean value indicating whether the query is a modifying query. + * @since 2.2 + */ + protected abstract boolean isModifyingQuery(); + private static CassandraConverter getRequiredConverter(ReactiveCassandraOperations operations) { Assert.notNull(operations, "ReactiveCassandraOperations must not be null"); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java index 1bee6dfc6..a793398eb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java @@ -104,6 +104,10 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); } + if (getTree().isDelete()) { + return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor); + } + return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor, getQueryMethod().getResultProcessor()); } @@ -131,4 +135,12 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { protected boolean isLimiting() { return getTree().isLimiting(); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isModifyingQuery() + */ + @Override + protected boolean isModifyingQuery() { + return getTree().isDelete(); + } } 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 2a4426e23..4e93da0d6 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 @@ -118,6 +118,32 @@ class QueryStatementCreator { return doWithQuery(parameterAccessor, tree, function); } + /** + * Create a {@literal DELETE} {@link Statement} from a {@link PartTree} and apply query options for delete query + * execution. + * + * @param statementFactory must not be {@literal null}. + * @param tree must not be {@literal null}. + * @param parameterAccessor must not be {@literal null}. + * @return the {@literal DELETE} {@link Statement}. + * @since 2.2 + */ + Statement delete(StatementFactory statementFactory, PartTree tree, CassandraParameterAccessor parameterAccessor) { + + Function function = query -> { + + RegularStatement statement = statementFactory.delete(query, requirePersistentEntity()); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query [%s].", statement)); + } + + return statement; + }; + + return doWithQuery(parameterAccessor, tree, function); + } + /** * Create a {@literal SELECT} {@link Statement} from a {@link PartTree} and apply query options for exists query * execution. Limit results to a single row. diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java index c01484989..8e6a38e4c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java @@ -17,10 +17,13 @@ package org.springframework.data.cassandra.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import org.reactivestreams.Publisher; + import org.springframework.core.convert.converter.Converter; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; @@ -190,7 +193,6 @@ interface ReactiveCassandraQueryExecution { * (non-Javadoc) * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ - @SuppressWarnings("ConstantConditions") @Override public Object execute(Statement statement, Class type) { return converter.convert(delegate.execute(statement, type)); @@ -222,6 +224,16 @@ interface ReactiveCassandraQueryExecution { return source; } + if (returnedType.getReturnedType().equals(Void.class)) { + if (source instanceof Mono) { + return ((Mono) source).then(); + } + + if (source instanceof Publisher) { + return Flux.from((Publisher) source).then(); + } + } + if (returnedType.isInstance(source)) { return source; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java index 6dba7c46e..fe56203b3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java @@ -104,6 +104,10 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); } + if (getTree().isDelete()) { + return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor); + } + return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor, getQueryMethod().getResultProcessor()); } @@ -131,4 +135,12 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue protected boolean isLimiting() { return getTree().isLimiting(); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isModifyingQuery() + */ + @Override + protected boolean isModifyingQuery() { + return getTree().isDelete(); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java index e74927d42..d5bb17923 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java @@ -140,4 +140,12 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra protected boolean isLimiting() { return false; } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isModifyingQuery() + */ + @Override + protected boolean isModifyingQuery() { + return false; + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java index 7e6377687..23547b1b8 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -134,4 +134,12 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { protected boolean isLimiting() { return false; } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isModifyingQuery() + */ + @Override + protected boolean isModifyingQuery() { + return false; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/QueryDerivationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/QueryDerivationIntegrationTests.java index 788196fc4..9d5c93435 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/QueryDerivationIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/QueryDerivationIntegrationTests.java @@ -343,6 +343,23 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC assertThat(count).isEqualTo(3); } + @Test // DATACASS-611 + public void shouldDeleteRecords() { + + personRepository.deleteByLastname("White"); + + assertThat(personRepository.countByLastname("White")).isZero(); + } + + @Test // DATACASS-611 + public void shouldDeleteRecordsWithWasApplied() { + + boolean deleted = personRepository.deleteByLastname("White"); + + assertThat(deleted).isTrue(); + assertThat(personRepository.countByLastname("White")).isZero(); + } + @Test // DATACASS-512 public void shouldApplyExistsProjection() { @@ -383,6 +400,10 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC long countByLastname(String lastname); + boolean deleteByLastname(String lastname); + + void deleteVoidByLastname(String lastname); + boolean existsByLastname(String lastname); Slice findAllSlicedByLastname(String lastname, Pageable pageable); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java index 3d95b09e7..49908333a 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/ReactiveCassandraRepositoryIntegrationTests.java @@ -15,19 +15,19 @@ */ package org.springframework.data.cassandra.repository; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.reactivestreams.Publisher; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -132,11 +132,10 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac repository.findByLastname(dave.getLastname()).as(StepVerifier::create).expectNextCount(2).verifyComplete(); } - @Test //DATACASS-529 + @Test // DATACASS-529 public void shouldFindSliceByLastName() { repository.findByLastname(carter.getLastname(), CassandraPageRequest.first(1)).as(StepVerifier::create) - .expectNextMatches(users -> users.getSize() == 1 && users.hasNext()) - .verifyComplete(); + .expectNextMatches(users -> users.getSize() == 1 && users.hasNext()).verifyComplete(); } @Test // DATACASS-529 @@ -193,8 +192,7 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac .verifyComplete(); groupRepostitory.findByIdGroupnameAndIdHashPrefix("Simpsons", "hash", Sort.by("id.username").descending()) - .as(StepVerifier::create) - .expectNext(new Group(key2), new Group(key1)) // + .as(StepVerifier::create).expectNext(new Group(key2), new Group(key1)) // .verifyComplete(); } @@ -208,6 +206,22 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac repository.countQueryByLastname("None").as(StepVerifier::create).expectNext(0L).verifyComplete(); } + @Test // DATACASS-611 + public void shouldDeleteRecords() { + + repository.deleteVoidById(dave.getId()).as(StepVerifier::create).verifyComplete(); + + repository.countByLastname("Matthews").as(StepVerifier::create).expectNext(1L).verifyComplete(); + } + + @Test // DATACASS-611 + public void shouldDeleteRecordsReturingWasApplied() { + + repository.deleteAllById(dave.getId()).as(StepVerifier::create).expectNext(true).verifyComplete(); + + repository.countByLastname("Matthews").as(StepVerifier::create).expectNext(1L).verifyComplete(); + } + @Test // DATACASS-512 public void shouldApplyExistsProjection() { @@ -232,6 +246,10 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac Mono countByLastname(String lastname); + Mono deleteAllById(String lastname); + + Mono deleteVoidById(String lastname); + Mono existsByLastname(String lastname); @Query("SELECT * FROM users WHERE lastname = ?0") diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java index 600a45ae1..6a0306c60 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -30,6 +30,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.convert.CassandraConverter; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; @@ -215,6 +216,15 @@ public class PartTreeCassandraQueryUnitTests { assertThat(statement.toString()).isEqualTo("SELECT COUNT(1) FROM person;"); } + @Test // DATACASS-611 + public void shouldCreateDeleteQuery() { + + Statement statement = deriveQueryFromMethod(Repo.class, "deleteAllByLastname", new Class[] { String.class }, + "Walter"); + + assertThat(statement.toString()).isEqualTo("DELETE FROM person WHERE lastname='Walter';"); + } + @Test // DATACASS-512 public void shouldCreateExistsQuery() { @@ -295,6 +305,8 @@ public class PartTreeCassandraQueryUnitTests { long countBy(); + boolean deleteAllByLastname(String lastname); + boolean existsBy(); @AllowFiltering diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java index 71d821c6a..c7b469504 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java @@ -141,6 +141,15 @@ public class ReactivePartTreeCassandraQueryUnitTests { assertThat(statement.toString()).isEqualTo("SELECT COUNT(1) FROM person;"); } + @Test // DATACASS-611 + public void shouldCreateDeleteQuery() { + + Statement statement = deriveQueryFromMethod(PartTreeCassandraQueryUnitTests.Repo.class, "deleteAllByLastname", + new Class[] { String.class }, "Walter"); + + assertThat(statement.toString()).isEqualTo("DELETE FROM person WHERE lastname='Walter';"); + } + @Test // DATACASS-512 public void shouldCreateExistsQuery() { @@ -202,6 +211,8 @@ public class ReactivePartTreeCassandraQueryUnitTests { Mono countBy(); + Mono deleteAllByLastname(String lastname); + Mono existsBy(); @Consistency(ConsistencyLevel.LOCAL_ONE) diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index b93b46054..200d64a75 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -13,6 +13,7 @@ This chapter summarizes changes and new features for each release. * Optimistic Locking support. * Auditing via `@EnableCassandraAuditing`. * Idempotency support in `@Query` annotation. +* Query derivation for <>. [[new-features.2-1-0]] == What's new in Spring Data for Apache Cassandra 2.1 diff --git a/src/main/asciidoc/reference/cassandra-repositories.adoc b/src/main/asciidoc/reference/cassandra-repositories.adoc index f9524505e..9db55825b 100644 --- a/src/main/asciidoc/reference/cassandra-repositories.adoc +++ b/src/main/asciidoc/reference/cassandra-repositories.adoc @@ -282,6 +282,25 @@ lower / upper bounds (`>` / `>=` & `<` / `<=`) according to `Range` |=== +[[cassandra.repositories.queries.delete]] +== Repository Delete Queries + +The keywords in the preceding table can be used in conjunction with `delete…By` to create queries that delete matching documents. + +==== +[source,java] +---- +interface PersonRepository extends Repository { + + void deleteWithoutResultByLastname(String lastname); + + boolean deleteByLastname(String lastname); +} +---- +==== + +Delete queries return whether the query was applied or terminate without returning a value using `void`. + include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2] [[cassandra.repositories.queries.options]] @@ -297,6 +316,7 @@ The declared consistency level is applied to the query each time it is executed. The following example sets the consistency level to `ConsistencyLevel.LOCAL_ONE`: ==== +[source,java] ---- public interface PersonRepository extends CrudRepository {