diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java index 9fc6aaffd..2d59f3def 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java @@ -230,7 +230,18 @@ public interface AsyncCassandraOperations { ListenableFuture count(Class entityClass) throws DataAccessException; /** - * Determine whether the row {@code entityClass} with the given {@code id} exists. + * Returns the number of rows for the given entity class applying {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass must not be {@literal null}. + * @return the number of existing entities. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + ListenableFuture count(Query query, Class entityClass) throws DataAccessException; + + /** + * Determine whether a row of {@code entityClass} with the given {@code id} exists. * * @param id the Id value. For single primary keys it's the plain value. For composite primary keys either the * {@link org.springframework.data.cassandra.core.mapping.PrimaryKeyClass} or @@ -241,6 +252,17 @@ public interface AsyncCassandraOperations { */ ListenableFuture exists(Object id, Class entityClass) throws DataAccessException; + /** + * Determine whether the result for {@code entityClass} {@link Query} yields at least one row. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return true, if the object exists. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + ListenableFuture exists(Query query, Class entityClass) throws DataAccessException; + /** * Execute the Select by {@code id} for the given {@code entityClass}. * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java index cf5a27643..a6e0f9133 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java @@ -48,6 +48,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.concurrent.ListenableFuture; +import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; @@ -405,6 +406,23 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { return getAsyncCqlOperations().queryForObject(select, Long.class); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#count(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public ListenableFuture count(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement count = statementFactory.count(query, + getMappingContext().getRequiredPersistentEntity(entityClass)); + + ListenableFuture result = getAsyncCqlOperations().queryForObject(count, Long.class); + + return new MappingListenableFutureAdapter<>(result, it -> it != null ? it : 0L); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#exists(java.lang.Object, java.lang.Class) */ @@ -424,6 +442,22 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { resultSet -> resultSet.iterator().hasNext()); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#exists(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public ListenableFuture exists(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement select = statementFactory.select(query.limit(1), + getMappingContext().getRequiredPersistentEntity(entityClass)); + + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().queryForResultSet(select), + resultSet -> resultSet.iterator().hasNext()); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOneById(java.lang.Object, java.lang.Class) */ @@ -537,8 +571,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); - Truncate truncate = QueryBuilder.truncate(getMappingContext().getRequiredPersistentEntity(entityClass) - .getTableName().toCql()); + Truncate truncate = QueryBuilder + .truncate(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(truncate), aBoolean -> null); } @@ -561,9 +595,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { } } - return getAsyncCqlOperations().execute((AsyncSessionCallback) session -> - AsyncResult.forValue(session.getCluster().getConfiguration().getQueryOptions().getFetchSize())) - .completable().join(); + return getAsyncCqlOperations().execute((AsyncSessionCallback) session -> AsyncResult + .forValue(session.getCluster().getConfiguration().getQueryOptions().getFetchSize())).completable().join(); } static class MappingListenableFutureAdapter diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java index e9281d42d..560da6178 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java @@ -257,7 +257,18 @@ public interface CassandraOperations { long count(Class entityClass) throws DataAccessException; /** - * Determine whether the row {@code entityClass} with the given {@code id} exists. + * Returns the number of rows for the given entity class applying {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass must not be {@literal null}. + * @return the number of existing entities. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + long count(Query query, Class entityClass) throws DataAccessException; + + /** + * Determine whether a row of {@code entityClass} with the given {@code id} exists. * * @param id the Id value. For single primary keys it's the plain value. For composite primary keys either the * {@link org.springframework.data.cassandra.core.mapping.PrimaryKeyClass} or @@ -268,6 +279,17 @@ public interface CassandraOperations { */ boolean exists(Object id, Class entityClass) throws DataAccessException; + /** + * Determine whether the result for {@code entityClass} {@link Query} yields at least one row. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return true, if the object exists. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + boolean exists(Query query, Class entityClass) throws DataAccessException; + /** * Execute the Select by {@code id} for the given {@code entityClass}. * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index 5bcc5d38f..4e84a2a01 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -15,13 +15,13 @@ */ package org.springframework.data.cassandra.core; +import lombok.NonNull; +import lombok.Value; + import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import lombok.NonNull; -import lombok.Value; - import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.core.convert.CassandraConverter; @@ -45,6 +45,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; @@ -386,6 +387,23 @@ public class CassandraTemplate implements CassandraOperations { return count != null ? count : 0L; } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#count(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public long count(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement count = statementFactory.count(query, + getMappingContext().getRequiredPersistentEntity(entityClass)); + + Long result = getCqlOperations().queryForObject(count, Long.class); + + return result != null ? result : 0L; + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraOperations#exists(java.lang.Object, java.lang.Class) */ @@ -404,6 +422,21 @@ public class CassandraTemplate implements CassandraOperations { return getCqlOperations().queryForResultSet(select).iterator().hasNext(); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#exists(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public boolean exists(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement select = statementFactory.select(query.limit(1), + getMappingContext().getRequiredPersistentEntity(entityClass)); + + return getCqlOperations().queryForResultSet(select).iterator().hasNext(); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraOperations#selectOneById(java.lang.Object, java.lang.Class) */ @@ -549,8 +582,8 @@ public class CassandraTemplate implements CassandraOperations { } } - return getCqlOperations().execute((SessionCallback) session -> - session.getCluster().getConfiguration().getQueryOptions().getFetchSize()); + return getCqlOperations().execute( + (SessionCallback) session -> session.getCluster().getConfiguration().getQueryOptions().getFetchSize()); } /* (non-Javadoc) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java index 61bf28e0f..df4e7f030 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java @@ -165,7 +165,18 @@ public interface ReactiveCassandraOperations { Mono count(Class entityClass) throws DataAccessException; /** - * Determine whether the row {@code entityClass} with the given {@code id} exists. + * Returns the number of rows for the given entity class applying {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass must not be {@literal null}. + * @return the number of existing entities. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + Mono count(Query query, Class entityClass) throws DataAccessException; + + /** + * Determine whether a row of {@code entityClass} with the given {@code id} exists. * * @param id the Id value. For single primary keys it's the plain value. For composite primary keys either the * {@link org.springframework.data.cassandra.core.mapping.PrimaryKeyClass} or @@ -176,6 +187,17 @@ public interface ReactiveCassandraOperations { */ Mono exists(Object id, Class entityClass) throws DataAccessException; + /** + * Determine whether the result for {@code entityClass} {@link Query} yields at least one row. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return true, if the object exists. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.1 + */ + Mono exists(Query query, Class entityClass) throws DataAccessException; + /** * Execute the Select by {@code id} for the given {@code entityClass}. * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java index d9c10db44..1bfcac50c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java @@ -17,7 +17,6 @@ package org.springframework.data.cassandra.core; import lombok.NonNull; import lombok.Value; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -45,6 +44,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; @@ -265,8 +265,8 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return select(getStatementFactory().select(query, - getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); + return select(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), + entityClass); } /* (non-Javadoc) @@ -278,8 +278,8 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return selectOne(getStatementFactory().select(query, - getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); + return selectOne(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), + entityClass); } /* (non-Javadoc) @@ -293,8 +293,8 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(update, "Update must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return getReactiveCqlOperations().execute(getStatementFactory().update(query, update, - getMappingContext().getRequiredPersistentEntity(entityClass))); + return getReactiveCqlOperations().execute( + getStatementFactory().update(query, update, getMappingContext().getRequiredPersistentEntity(entityClass))); } /* (non-Javadoc) @@ -328,6 +328,21 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { return getReactiveCqlOperations().queryForObject(select, Long.class); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#count(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Mono count(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement count = statementFactory.count(query, + getMappingContext().getRequiredPersistentEntity(entityClass)); + + return getReactiveCqlOperations().queryForObject(count, Long.class); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#exists(java.lang.Object, java.lang.Class) */ @@ -346,6 +361,21 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { return getReactiveCqlOperations().queryForRows(select).hasElements(); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#exists(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Mono exists(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + RegularStatement select = statementFactory.select(query.limit(1), + getMappingContext().getRequiredPersistentEntity(entityClass)); + + return getReactiveCqlOperations().queryForRows(select).hasElements(); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOneById(java.lang.Object, java.lang.Class) */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java index fce495f32..0e643c2b3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.core; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -140,10 +141,36 @@ public class StatementFactory { List selectors = getQueryMapper().getMappedSelectors(query.getColumns(), entity); + return createSelect(query, entity, filter, selectors); + } + + /** + * Create a {@literal COUNT} statement by mapping {@link Query} to {@link Select}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + * @since 2.1 + */ + public RegularStatement count(Query query, CassandraPersistentEntity entity) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "Entity must not be null"); + + Filter filter = getQueryMapper().getMappedObject(query, entity); + + List selectors = Collections.singletonList(FunctionCall.from("COUNT", 1L)); + + return createSelect(query, entity, filter, selectors); + } + + private Select createSelect(Query query, CassandraPersistentEntity entity, Filter filter, + List selectors) { + Sort sort = Optional.of(query.getSort()).map(querySort -> getQueryMapper().getMappedSort(querySort, entity)) .orElse(Sort.unsorted()); - Select select = select(selectors, entity.getTableName(), filter, sort); + Select select = createSelectAndOrder(selectors, entity.getTableName(), filter, sort); query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(select, queryOptions)); @@ -160,7 +187,7 @@ public class StatementFactory { return select; } - private static Select select(List selectors, CqlIdentifier from, Filter filter, Sort sort) { + private static Select createSelectAndOrder(List selectors, CqlIdentifier from, Filter filter, Sort sort) { Select select; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/CountQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/CountQuery.java new file mode 100644 index 000000000..0fae5f55e --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/CountQuery.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to declare count queries directly on repository methods. Both attributes allow using a placeholder + * notation of {@code ?0}, {@code ?1} and so on. + * + * @author Mark Paluch + * @since 2.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query(count = true) +public @interface CountQuery { + + /** + * A Cassandra CQL3 string to define the actual query to be executed. Placeholders {@code ?0}, {@code ?1}, etc are + * supported. + */ + @AliasFor(annotation = Query.class) + String value() default ""; +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/ExistsQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/ExistsQuery.java new file mode 100644 index 000000000..41552ad66 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/ExistsQuery.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to declare exists queries directly on repository methods. Both attributes allow using a placeholder + * notation of {@code ?0}, {@code ?1} and so on. + * + * @author Mark Paluch + * @since 2.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query(exists = true) +public @interface ExistsQuery { + + /** + * A Cassandra CQL3 string to define the actual query to be executed. Placeholders {@code ?0}, {@code ?1}, etc are + * supported. + */ + @AliasFor(annotation = Query.class) + String value() default ""; +} 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 6842a95f3..74bab9abf 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 @@ -48,4 +48,18 @@ public @interface Query { * @since 2.0 */ boolean allowFiltering() default false; + + /** + * Returns whether the query defined should be executed as count projection. + * + * @since 2.1 + */ + boolean count() default false; + + /** + * Returns whether the query defined should be executed as exists projection. + * + * @since 2.1 + */ + boolean exists() default false; } 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 d079ebbba..249da5d5c 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 @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.repository.query; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.CollectionExecution; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ExistsExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingConverter; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultSetQuery; @@ -52,7 +53,7 @@ public abstract class AbstractCassandraQuery extends CassandraRepositoryQuerySup */ public AbstractCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations) { - super(queryMethod); + super(queryMethod, operations.getConverter().getMappingContext()); Assert.notNull(operations, "CassandraOperations must not be null"); @@ -123,8 +124,28 @@ public abstract class AbstractCassandraQuery extends CassandraRepositoryQuerySup return new ResultSetQuery(getOperations()); } else if (getQueryMethod().isStreamQuery()) { return new StreamExecution(getOperations(), resultProcessing); + } else if (isCountQuery()) { + return ((statement, type) -> new SingleEntityExecution(getOperations()).execute(statement, Long.class)); + } else if (isExistsQuery()) { + return new ExistsExecution(getOperations()); } else { return new SingleEntityExecution(getOperations()); } } + + /** + * Returns whether the query should get a count projection applied. + * + * @return + * @since 2.1 + */ + protected abstract boolean isCountQuery(); + + /** + * Returns whether the query should get an exists projection applied. + * + * @return + * @since 2.1 + */ + protected abstract boolean isExistsQuery(); } 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 bd708555e..4ffb92a10 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 @@ -18,10 +18,12 @@ package org.springframework.data.cassandra.repository.query; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.CollectionExecution; +import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.ExistsExecution; import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.ResultProcessingConverter; import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.ResultProcessingExecution; import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.SingleEntityExecution; @@ -30,8 +32,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.util.Assert; -import org.reactivestreams.Publisher; - import com.datastax.driver.core.Statement; /** @@ -54,7 +54,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository */ public AbstractReactiveCassandraQuery(ReactiveCassandraQueryMethod method, ReactiveCassandraOperations operations) { - super(method); + super(method, operations.getConverter().getMappingContext()); Assert.notNull(operations, "ReactiveCassandraOperations must not be null"); @@ -134,9 +134,33 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository return new ResultProcessingExecution(getExecutionToWrap(), resultProcessing); } - /* (non-Javadoc) */ private ReactiveCassandraQueryExecution getExecutionToWrap() { - return (getQueryMethod().isCollectionQuery() ? new CollectionExecution(getReactiveCassandraOperations()) - : new SingleEntityExecution(getReactiveCassandraOperations())); + + if (getQueryMethod().isCollectionQuery()) { + return new CollectionExecution(getReactiveCassandraOperations()); + } else if (isCountQuery()) { + return ((statement, type) -> new SingleEntityExecution(getReactiveCassandraOperations()).execute(statement, + Long.class)); + } else if (isExistsQuery()) { + return new ExistsExecution(getReactiveCassandraOperations()); + } else { + return new SingleEntityExecution(getReactiveCassandraOperations()); + } } + + /** + * Returns whether the query should get a count projection applied. + * + * @return + * @since 2.1 + */ + protected abstract boolean isCountQuery(); + + /** + * Returns whether the query should get an exists projection applied. + * + * @return + * @since 2.1 + */ + protected abstract boolean isExistsQuery(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java index b765ba1be..513cb376f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java @@ -18,6 +18,8 @@ package org.springframework.data.cassandra.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import java.util.Iterator; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; @@ -31,6 +33,8 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; import com.datastax.driver.core.Statement; /** @@ -132,6 +136,44 @@ interface CassandraQueryExecution { } } + /** + * {@link CassandraQueryExecution} for an Exists query supporting count and regular row-data for exists calculation. + * + * @author Mark Paluch + * @since 2.1 + */ + @RequiredArgsConstructor + final class ExistsExecution implements CassandraQueryExecution { + + private final @NonNull CassandraOperations operations; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) + */ + @Override + public Object execute(Statement statement, Class type) { + + ResultSet resultSet = operations.getCqlOperations().queryForResultSet(statement); + + Iterator iterator = resultSet.iterator(); + + if (iterator.hasNext()) { + + Row row = iterator.next(); + + if (!iterator.hasNext() && ProjectionUtil.isCountProjection(row)) { + + Object object = row.getObject(0); + return ((Number) object).longValue() > 0; + } + + return true; + } + + return false; + } + } + /** * {@link CassandraQueryExecution} to return a {@link com.datastax.driver.core.ResultSet}. * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraRepositoryQuerySupport.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraRepositoryQuerySupport.java index 75b64f165..8d4d8f5ef 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraRepositoryQuerySupport.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraRepositoryQuerySupport.java @@ -15,21 +15,24 @@ */ package org.springframework.data.cassandra.repository.query; -import java.util.Map; - import lombok.RequiredArgsConstructor; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Base class for Cassandra {@link RepositoryQuery} implementations providing common infrastructure such as * {@link EntityInstantiators} and {@link QueryStatementCreator}. @@ -53,15 +56,35 @@ public abstract class CassandraRepositoryQuerySupport implements RepositoryQuery * {@link CassandraOperations}. * * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. + * @deprecated use {@link #CassandraRepositoryQuerySupport(CassandraQueryMethod, MappingContext)} */ + @Deprecated public CassandraRepositoryQuerySupport(CassandraQueryMethod queryMethod) { Assert.notNull(queryMethod, "CassandraQueryMethod must not be null"); this.queryMethod = queryMethod; this.instantiators = new EntityInstantiators(); - this.queryStatementCreator = new QueryStatementCreator(queryMethod); + this.queryStatementCreator = new QueryStatementCreator(queryMethod, new CassandraMappingContext()); + } + + /** + * Create a new {@link AbstractCassandraQuery} from the given {@link CassandraQueryMethod} and + * {@link CassandraOperations}. + * + * @param queryMethod must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @since 2.1 + */ + public CassandraRepositoryQuerySupport(CassandraQueryMethod queryMethod, + MappingContext, CassandraPersistentProperty> mappingContext) { + + Assert.notNull(queryMethod, "CassandraQueryMethod must not be null"); + Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); + + this.queryMethod = queryMethod; + this.instantiators = new EntityInstantiators(); + this.queryStatementCreator = new QueryStatementCreator(queryMethod, mappingContext); } /* (non-Javadoc) 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 26da892ba..dbe8f0406 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 @@ -95,7 +95,31 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { */ @Override protected Statement createQuery(CassandraParameterAccessor parameterAccessor) { - return getQueryStatementCreator().select(getStatementFactory(), getTree(), getMappingContext(), - parameterAccessor); + + if (isCountQuery()) { + return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor); + } + + if (isExistsQuery()) { + return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); + } + + return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ProjectionUtil.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ProjectionUtil.java new file mode 100644 index 000000000..5700eddb5 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ProjectionUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Row; + +/** + * Utility methods for projections. + * + * @author Mark Paluch + * @since 2.1 + */ +@UtilityClass +class ProjectionUtil { + + private final static Set NUMERIC_TYPES = new HashSet<>(Arrays.asList(DataType.bigint(), DataType.varint(), + DataType.smallint(), DataType.cint(), DataType.counter(), DataType.tinyint())); + + /** + * Determine wether the {@link Row} qualifies for a count projection. Count projection candidates have a single + * numeric column. + * + * @param row + * @return + */ + static boolean isCountProjection(Row row) { + + ColumnDefinitions columnDefinitions = row.getColumnDefinitions(); + return columnDefinitions.size() == 1 && NUMERIC_TYPES.contains(columnDefinitions.getType(0)); + } + + /** + * Determine whether multiple {@code boolean} flags are set. Allowed is at most a single {@literal true} value. + * + * @param flags + * @return {@literal true} if more than one {@code flag} is set to {@literal true}. + */ + static boolean hasAmbiguousProjectionFlags(Boolean... flags) { + return Arrays.stream(flags).filter(Boolean::booleanValue).count() > 1; + } +} 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 f6b269a2e..1a03df846 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 @@ -15,10 +15,13 @@ */ package org.springframework.data.cassandra.repository.query; -import java.util.Optional; - import lombok.RequiredArgsConstructor; +import java.util.Optional; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.core.StatementFactory; import org.springframework.data.cassandra.core.cql.QueryOptions; import org.springframework.data.cassandra.core.cql.QueryOptionsUtil; @@ -29,9 +32,6 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.parser.PartTree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; @@ -50,19 +50,104 @@ class QueryStatementCreator { private final CassandraQueryMethod queryMethod; + private final MappingContext, CassandraPersistentProperty> mappingContext; + /** * Create a {@literal SELECT} {@link Statement} from a {@link PartTree} and apply query options. * * @param statementFactory must not be {@literal null}. * @param tree must not be {@literal null}. - * @param mappingContext must not be {@literal null}. * @param parameterAccessor must not be {@literal null}. * @return the {@literal SELECT} {@link Statement}. */ Statement select(StatementFactory statementFactory, PartTree tree, - MappingContext, CassandraPersistentProperty> mappingContext, CassandraParameterAccessor parameterAccessor) { + Function function = query -> { + + CassandraPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(queryMethod.getDomainClass()); + + RegularStatement statement = statementFactory.select(query, persistentEntity); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query [%s].", statement)); + } + + return statement; + }; + + return doWithQuery(parameterAccessor, tree, function); + } + + /** + * Create a {@literal COUNT} {@link Statement} from a {@link PartTree} and apply query options. + * + * @param statementFactory must not be {@literal null}. + * @param tree must not be {@literal null}. + * @param parameterAccessor must not be {@literal null}. + * @return the {@literal SELECT} {@link Statement}. + * @since 2.1 + */ + Statement count(StatementFactory statementFactory, PartTree tree, CassandraParameterAccessor parameterAccessor) { + + Function function = query -> { + + CassandraPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(queryMethod.getDomainClass()); + + RegularStatement statement = statementFactory.count(query, persistentEntity); + + 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. + * + * @param statementFactory must not be {@literal null}. + * @param tree must not be {@literal null}. + * @param parameterAccessor must not be {@literal null}. + * @return the {@literal SELECT} {@link Statement}. + * @since 2.1 + */ + Statement exists(StatementFactory statementFactory, PartTree tree, CassandraParameterAccessor parameterAccessor) { + + Function function = query -> { + + CassandraPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(queryMethod.getDomainClass()); + + RegularStatement statement = statementFactory.select(query.limit(1), persistentEntity); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query [%s].", statement)); + } + + return statement; + }; + + return doWithQuery(parameterAccessor, tree, function); + } + + /** + * A {@link Function} to {@link Query} derived from a {@link PartTree} and apply query options. + * + * @param parameterAccessor must not be {@literal null}. + * @param tree must not be {@literal null}. + * @param function callback function must not be {@literal null}. + * @return the {@literal SELECT} {@link Statement}. + */ + T doWithQuery(CassandraParameterAccessor parameterAccessor, PartTree tree, + Function function) { + CassandraQueryCreator queryCreator = new CassandraQueryCreator(tree, parameterAccessor, mappingContext); Query query = queryCreator.createQuery(); @@ -89,17 +174,7 @@ class QueryStatementCreator { .build()); } - CassandraPersistentEntity persistentEntity = - mappingContext.getRequiredPersistentEntity(queryMethod.getDomainClass()); - - RegularStatement statement = statementFactory.select(query, persistentEntity); - - if (LOG.isDebugEnabled()) { - LOG.debug(String.format("Created query [%s].", statement)); - } - - return statement; - + return function.apply(query); } catch (RuntimeException e) { throw QueryCreationException.create(queryMethod, e); } 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 68ead37f9..0d1bc8d42 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,6 +17,9 @@ package org.springframework.data.cassandra.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +import java.util.List; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; @@ -28,6 +31,7 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.Row; import com.datastax.driver.core.Statement; /** @@ -81,6 +85,48 @@ interface ReactiveCassandraQueryExecution { } } + /** + * {@link ReactiveCassandraQueryExecution} for an Exists query supporting count and regular row-data for exists + * calculation. + * + * @author Mark Paluch + * @since 2.1 + */ + @RequiredArgsConstructor + final class ExistsExecution implements ReactiveCassandraQueryExecution { + + private final @NonNull ReactiveCassandraOperations operations; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(com.datastax.driver.core.Statement, java.lang.Class) + */ + @Override + public Object execute(Statement statement, Class type) { + + Mono> rows = operations.getReactiveCqlOperations().queryForRows(statement).buffer(2).next(); + + return rows.map(it -> { + + if (it.isEmpty()) { + return false; + } + + if (it.size() == 1) { + + Row row = it.get(0); + + if (ProjectionUtil.isCountProjection(row)) { + + Object object = row.getObject(0); + return ((Number) object).longValue() > 0; + } + } + + return true; + }).switchIfEmpty(Mono.just(false)); + } + } + /** * An {@link ReactiveCassandraQueryExecution} that wraps the results of the given delegate with the given result * processing. 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 d1a826f17..c999d2576 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 @@ -95,6 +95,31 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue */ @Override protected Statement createQuery(CassandraParameterAccessor parameterAccessor) { - return getQueryStatementCreator().select(getStatementFactory(), getTree(), getMappingContext(), parameterAccessor); + + if (isCountQuery()) { + return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor); + } + + if (isExistsQuery()) { + return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor); + } + + return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractReactiveCassandraQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractReactiveCassandraQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); } } 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 39f4f1fe9..5eec08c9d 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 @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.repository.query; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.repository.Query; import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -36,8 +37,14 @@ import com.datastax.driver.core.SimpleStatement; */ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandraQuery { + private static final String COUNT_AND_EXISTS = "Manually defined query for %s cannot be a count and exists query at the same time!"; + private final StringBasedQuery stringBasedQuery; + private final boolean isCountQuery; + + private final boolean isExistsQuery; + /** * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, * {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. @@ -59,22 +66,38 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra * Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod}, * {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. * - * @param queryMethod {@link ReactiveCassandraQueryMethod} on which this query is based. + * @param method {@link ReactiveCassandraQueryMethod} on which this query is based. * @param operations {@link ReactiveCassandraOperations} used to perform data access in Cassandra. * @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query. * @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. */ - public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod queryMethod, + public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod method, ReactiveCassandraOperations operations, SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) { - super(queryMethod, operations); + super(method, operations); Assert.hasText(query, "Query must not be empty"); this.stringBasedQuery = new StringBasedQuery(query, new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); + + if (method.hasAnnotatedQuery()) { + + Query queryAnnotation = method.getQueryAnnotation().get(); + + this.isCountQuery = queryAnnotation.count(); + this.isExistsQuery = queryAnnotation.exists(); + + if (ProjectionUtil.hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery)) { + throw new IllegalArgumentException(String.format(COUNT_AND_EXISTS, method)); + } + } else { + + this.isCountQuery = false; + this.isExistsQuery = false; + } } protected StringBasedQuery getStringBasedQuery() { @@ -88,4 +111,20 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractReactiveCassandraQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return isCountQuery; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractReactiveCassandraQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return isExistsQuery; + } } 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 f7cb50950..ed8513dd9 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 @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.repository.query; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.Query; import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -35,8 +36,14 @@ import com.datastax.driver.core.SimpleStatement; */ public class StringBasedCassandraQuery extends AbstractCassandraQuery { + private static final String COUNT_AND_EXISTS = "Manually defined query for %s cannot be a count and exists query at the same time!"; + private final StringBasedQuery stringBasedQuery; + private final boolean isCountQuery; + + private final boolean isExistsQuery; + /** * Create a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. @@ -58,19 +65,35 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. * * @param query - * @param queryMethod {@link CassandraQueryMethod} on which this query is based. + * @param method {@link CassandraQueryMethod} on which this query is based. * @param operations {@link CassandraOperations} used to perform data access in Cassandra. * @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query. * @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared * {@link org.springframework.expression.spel.support.StandardEvaluationContext}. */ - public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations, + public StringBasedCassandraQuery(String query, CassandraQueryMethod method, CassandraOperations operations, SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) { - super(queryMethod, operations); + super(method, operations); this.stringBasedQuery = new StringBasedQuery(query, new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); + + if (method.hasAnnotatedQuery()) { + + Query queryAnnotation = method.getQueryAnnotation().get(); + + this.isCountQuery = queryAnnotation.count(); + this.isExistsQuery = queryAnnotation.exists(); + + if (ProjectionUtil.hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery)) { + throw new IllegalArgumentException(String.format(COUNT_AND_EXISTS, method)); + } + } else { + + this.isCountQuery = false; + this.isExistsQuery = false; + } } protected StringBasedQuery getStringBasedQuery() { @@ -84,4 +107,20 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return isCountQuery; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return isExistsQuery; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java index 59308e01d..4c890c5c9 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.cassandra.core; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.cassandra.core.query.Criteria.*; import java.util.HashSet; import java.util.LinkedHashSet; @@ -24,12 +25,10 @@ import java.util.concurrent.Future; import org.junit.Before; import org.junit.Test; - import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.AsyncCqlTemplate; import org.springframework.data.cassandra.core.query.CassandraPageRequest; import org.springframework.data.cassandra.core.query.Columns; -import org.springframework.data.cassandra.core.query.Criteria; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.User; @@ -52,7 +51,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea private AsyncCassandraTemplate template; @Before - public void setUp() throws Exception { + public void setUp() { MappingCassandraConverter converter = new MappingCassandraConverter(); CassandraTemplate cassandraTemplate = new CassandraTemplate(session, converter); @@ -80,7 +79,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea getUninterruptibly(template.insert(token1)); getUninterruptibly(template.insert(token2)); - Query query = Query.query(Criteria.where("userId").is(token1.getUserId())).sort(Sort.by("token")); + Query query = Query.query(where("userId").is(token1.getUserId())).sort(Sort.by("token")); assertThat(getUninterruptibly(template.select(query, UserToken.class))).containsSequence(token1, token2); } @@ -95,7 +94,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea getUninterruptibly(template.insert(token1)); - Query query = Query.query(Criteria.where("userId").is(token1.getUserId())); + Query query = Query.query(where("userId").is(token1.getUserId())); assertThat(getUninterruptibly(template.selectOne(query, UserToken.class))).isEqualTo(token1); } @@ -143,7 +142,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea } @Test // DATACASS-292 - public void shouldInsertAndCountEntities() throws Exception { + public void shouldInsertAndCountEntities() { User user = new User("heisenberg", "Walter", "White"); @@ -153,8 +152,30 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea assertThat(getUninterruptibly(count)).isEqualTo(1L); } + @Test // DATACASS-512 + public void shouldInsertEntityAndCountByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user)); + + assertThat(getUninterruptibly(template.count(Query.query(where("id").is("heisenberg")), User.class))).isOne(); + assertThat(getUninterruptibly(template.count(Query.query(where("id").is("foo")), User.class))).isZero(); + } + + @Test // DATACASS-512 + public void shouldInsertEntityAndExistsByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user)); + + assertThat(getUninterruptibly(template.exists(Query.query(where("id").is("heisenberg")), User.class))).isTrue(); + assertThat(getUninterruptibly(template.exists(Query.query(where("id").is("foo")), User.class))).isFalse(); + } + @Test // DATACASS-292 - public void updateShouldUpdateEntity() throws Exception { + public void updateShouldUpdateEntity() { User user = new User("heisenberg", "Walter", "White"); getUninterruptibly(template.insert(user)); @@ -197,12 +218,12 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea } @Test // DATACASS-343 - public void updateShouldUpdateEntityByQuery() throws Exception { + public void updateShouldUpdateEntityByQuery() { User user = new User("heisenberg", "Walter", "White"); - template.insert(user).get(); + getUninterruptibly(template.insert(user)); - Query query = Query.query(Criteria.where("id").is("heisenberg")); + Query query = Query.query(where("id").is("heisenberg")); boolean result = getUninterruptibly( template.update(query, Update.empty().set("firstname", "Walter Hartwell"), User.class)); assertThat(result).isTrue(); @@ -211,24 +232,24 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea } @Test // DATACASS-343 - public void deleteByQueryShouldRemoveEntity() throws Exception { + public void deleteByQueryShouldRemoveEntity() { User user = new User("heisenberg", "Walter", "White"); - template.insert(user).get(); + getUninterruptibly(template.insert(user)); - Query query = Query.query(Criteria.where("id").is("heisenberg")); + Query query = Query.query(where("id").is("heisenberg")); assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); assertThat(getUser(user.getId())).isNull(); } @Test // DATACASS-343 - public void deleteColumnsByQueryShouldRemoveColumn() throws Exception { + public void deleteColumnsByQueryShouldRemoveColumn() { User user = new User("heisenberg", "Walter", "White"); - template.insert(user).get(); + getUninterruptibly(template.insert(user)); - Query query = Query.query(Criteria.where("id").is("heisenberg")).columns(Columns.from("lastname")); + Query query = Query.query(where("id").is("heisenberg")).columns(Columns.from("lastname")); assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); @@ -238,7 +259,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea } @Test // DATACASS-292 - public void deleteShouldRemoveEntity() throws Exception { + public void deleteShouldRemoveEntity() { User user = new User("heisenberg", "Walter", "White"); getUninterruptibly(template.insert(user)); @@ -250,7 +271,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea } @Test // DATACASS-292 - public void deleteByIdShouldRemoveEntity() throws Exception { + public void deleteByIdShouldRemoveEntity() { User user = new User("heisenberg", "Walter", "White"); getUninterruptibly(template.insert(user)); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java index fdae186e6..b2b9c73d9 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java @@ -37,6 +37,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.cassandra.CassandraConnectionFailureException; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.domain.User; import org.springframework.util.concurrent.ListenableFuture; @@ -208,6 +209,18 @@ public class AsyncCassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users WHERE id='myid';"); } + @Test // DATACASS-512 + public void existsByQueryShouldReturnExistingElement() { + + when(resultSet.iterator()).thenReturn(Collections.singleton(row).iterator()); + + ListenableFuture future = template.exists(Query.empty(), User.class); + + assertThat(getUninterruptibly(future)).isTrue(); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users LIMIT 1;"); + } + @Test // DATACASS-292 public void countShouldExecuteCountQueryElement() { @@ -222,6 +235,20 @@ public class AsyncCassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT count(*) FROM users;"); } + @Test // DATACASS-292 + public void countByQueryShouldExecuteCountQueryElement() { + + when(resultSet.iterator()).thenReturn(Collections.singleton(row).iterator()); + when(row.getLong(0)).thenReturn(42L); + when(columnDefinitions.size()).thenReturn(1); + + ListenableFuture future = template.count(Query.empty(), User.class); + + assertThat(getUninterruptibly(future)).isEqualTo(42L); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT COUNT(1) FROM users;"); + } + @Test // DATACASS-292 public void insertShouldInsertEntity() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java index 913bee29f..7073ab9bb 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java @@ -15,8 +15,9 @@ */ package org.springframework.data.cassandra.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assume.assumeTrue; +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; +import static org.springframework.data.cassandra.core.query.Criteria.*; import java.util.Arrays; import java.util.Collections; @@ -29,13 +30,11 @@ import java.util.stream.Stream; import org.junit.Before; import org.junit.Test; - import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.CqlTemplate; import org.springframework.data.cassandra.core.mapping.BasicMapId; import org.springframework.data.cassandra.core.query.CassandraPageRequest; import org.springframework.data.cassandra.core.query.Columns; -import org.springframework.data.cassandra.core.query.Criteria; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.BookReference; @@ -93,8 +92,8 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI template.insert(userToken); - Query query = Query.query(Criteria.where("userId").is(userToken.getUserId())) - .and(Criteria.where("userComment").is("cook")).withAllowFiltering(); + Query query = Query.query(where("userId").is(userToken.getUserId())).and(where("userComment").is("cook")) + .withAllowFiltering(); UserToken loaded = template.selectOne(query, UserToken.class); assertThat(loaded).isNotNull(); @@ -117,7 +116,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI template.insert(token1); template.insert(token2); - Query query = Query.query(Criteria.where("userId").is(token1.getUserId())).sort(Sort.by("token")); + Query query = Query.query(where("userId").is(token1.getUserId())).sort(Sort.by("token")); List loaded = template.select(query, UserToken.class); assertThat(loaded).containsSequence(token1, token2); @@ -133,7 +132,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI template.insert(token1); - Query query = Query.query(Criteria.where("userId").is(token1.getUserId())); + Query query = Query.query(where("userId").is(token1.getUserId())); UserToken loaded = template.selectOne(query, UserToken.class); assertThat(loaded).isEqualTo(token1); @@ -191,6 +190,28 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(count).isEqualTo(1L); } + @Test // DATACASS-512 + public void shouldInsertEntityAndCountByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + template.insert(user); + + assertThat(template.count(Query.query(where("id").is("heisenberg")), User.class)).isOne(); + assertThat(template.count(Query.query(where("id").is("foo")), User.class)).isZero(); + } + + @Test // DATACASS-512 + public void shouldInsertEntityAndExistsByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + template.insert(user); + + assertThat(template.exists(Query.query(where("id").is("heisenberg")), User.class)).isTrue(); + assertThat(template.exists(Query.query(where("id").is("foo")), User.class)).isFalse(); + } + @Test // DATACASS-292 public void updateShouldUpdateEntity() { @@ -240,7 +261,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI User person = new User("heisenberg", "Walter", "White"); template.insert(person); - Query query = Query.query(Criteria.where("id").is("heisenberg")); + Query query = Query.query(where("id").is("heisenberg")); boolean result = template.update(query, Update.empty().set("firstname", "Walter Hartwell"), User.class); assertThat(result).isTrue(); @@ -253,7 +274,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI User user = new User("heisenberg", "Walter", "White"); template.insert(user); - Query query = Query.query(Criteria.where("id").is("heisenberg")); + Query query = Query.query(where("id").is("heisenberg")); assertThat(template.delete(query, User.class)).isTrue(); assertThat(template.selectOneById(user.getId(), User.class)).isNull(); @@ -265,7 +286,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI User user = new User("heisenberg", "Walter", "White"); template.insert(user); - Query query = Query.query(Criteria.where("id").is("heisenberg")).columns(Columns.from("lastname")); + Query query = Query.query(where("id").is("heisenberg")).columns(Columns.from("lastname")); assertThat(template.delete(query, User.class)).isTrue(); @@ -314,7 +335,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI User person = new User("heisenberg", "Walter", "White"); template.insert(person); - Query query = Query.query(Criteria.where("id").is("heisenberg")); + Query query = Query.query(where("id").is("heisenberg")); Stream stream = template.stream(query, User.class); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java index 65d3c25e1..7a6069034 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java @@ -34,6 +34,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.cassandra.CassandraConnectionFailureException; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.domain.User; import com.datastax.driver.core.ColumnDefinitions; @@ -175,6 +176,18 @@ public class CassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users WHERE id='myid';"); } + @Test // DATACASS-512 + public void existsByQueryShouldReturnExistingElement() { + + when(resultSet.iterator()).thenReturn(Collections.singleton(row).iterator()); + + boolean exists = template.exists(Query.empty(), User.class); + + assertThat(exists).isTrue(); + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users LIMIT 1;"); + } + @Test // DATACASS-292 public void countShouldExecuteCountQueryElement() { @@ -189,6 +202,20 @@ public class CassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT count(*) FROM users;"); } + @Test // DATACASS-512 + public void countByQueryShouldExecuteCountQueryElement() { + + when(resultSet.iterator()).thenReturn(Collections.singleton(row).iterator()); + when(row.getLong(0)).thenReturn(42L); + when(columnDefinitions.size()).thenReturn(1); + + long count = template.count(Query.empty(), User.class); + + assertThat(count).isEqualTo(42L); + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT COUNT(1) FROM users;"); + } + @Test // DATACASS-292 public void insertShouldInsertEntity() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java index 8ab8c2d12..4601d9774 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.core; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.cassandra.core.query.Criteria.*; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -49,7 +50,7 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC ReactiveCassandraTemplate template; @Before - public void setUp() throws Exception { + public void setUp() { MappingCassandraConverter converter = new MappingCassandraConverter(); CassandraTemplate cassandraTemplate = new CassandraTemplate(this.session, converter); @@ -107,7 +108,7 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC } @Test // DATACASS-335 - public void shouldInsertAndCountEntities() { + public void shouldInsertEntityAndCount() { User user = new User("heisenberg", "Walter", "White"); @@ -116,6 +117,38 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.count(User.class)).expectNext(1L).verifyComplete(); } + @Test // DATACASS-335 + public void shouldInsertEntityAndCountByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + StepVerifier.create(template.insert(user)).expectNextCount(1).verifyComplete(); + + StepVerifier.create(template.count(Query.query(where("id").is("heisenberg")), User.class)) // + .expectNext(1L) // + .verifyComplete(); + + StepVerifier.create(template.count(Query.query(where("id").is("foo")), User.class)) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATACASS-335 + public void shouldInsertAndExistsByQueryEntities() { + + User user = new User("heisenberg", "Walter", "White"); + + StepVerifier.create(template.insert(user)).expectNextCount(1).verifyComplete(); + + StepVerifier.create(template.exists(Query.query(where("id").is("heisenberg")), User.class)) // + .expectNext(true) // + .verifyComplete(); + + StepVerifier.create(template.exists(Query.query(where("id").is("foo")), User.class)) // + .expectNext(false) // + .verifyComplete(); + } + @Test // DATACASS-335 public void updateShouldUpdateEntity() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java index 11f601e5d..82bc1379f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java @@ -36,6 +36,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.cassandra.ReactiveResultSet; import org.springframework.data.cassandra.ReactiveSession; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.domain.User; import com.datastax.driver.core.ColumnDefinitions; @@ -149,6 +150,28 @@ public class ReactiveCassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users WHERE id='myid';"); } + @Test // DATACASS-512 + public void existsByQueryShouldReturnExistingElement() { + + when(reactiveResultSet.rows()).thenReturn(Flux.just(row)); + + StepVerifier.create(template.exists(Query.empty(), User.class)).expectNext(true).verifyComplete(); + + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users LIMIT 1;"); + } + + @Test // DATACASS-512 + public void existsByQueryShouldReturnNonExistingElement() { + + when(reactiveResultSet.rows()).thenReturn(Flux.empty()); + + StepVerifier.create(template.exists(Query.empty(), User.class)).expectNext(false).verifyComplete(); + + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users LIMIT 1;"); + } + @Test // DATACASS-335 public void countShouldExecuteCountQueryElement() { @@ -162,6 +185,19 @@ public class ReactiveCassandraTemplateUnitTests { assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT count(*) FROM users;"); } + @Test // DATACASS-512 + public void countByQueryShouldExecuteCountQueryElement() { + + when(reactiveResultSet.rows()).thenReturn(Flux.just(row)); + when(row.getLong(0)).thenReturn(42L); + when(columnDefinitions.size()).thenReturn(1); + + StepVerifier.create(template.count(Query.empty(), User.class)).expectNext(42L).verifyComplete(); + + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT COUNT(1) FROM users;"); + } + @Test // DATACASS-335 public void insertShouldInsertEntity() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java index c57c2c9ca..618902530 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java @@ -229,6 +229,17 @@ public class StatementFactoryUnitTests { assertThat(update.toString()).isEqualTo("UPDATE person SET number=number-1;"); } + @Test // DATACASS-512 + public void shouldCreateCountQuery() { + + Query query = Query.query(Criteria.where("foo").is("bar")); + + Statement count = statementFactory.count(query, + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(count.toString()).isEqualTo("SELECT COUNT(1) FROM group WHERE foo='bar';"); + } + static class Person { @Id String id; 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 f38086a4b..fbfcec478 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 @@ -15,8 +15,8 @@ */ package org.springframework.data.cassandra.repository; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assume.assumeTrue; +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; import java.time.LocalDate; import java.util.ArrayList; @@ -26,10 +26,10 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; @@ -55,8 +55,6 @@ import org.springframework.data.util.Version; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.assertj.core.api.Assertions; - import com.datastax.driver.core.Session; /** @@ -183,7 +181,7 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC Collection collection = personRepository.findPersonProjectedBy(); - Assertions.assertThat(collection).hasSize(3).extracting("firstname").contains(flynn.getFirstname(), + assertThat(collection).hasSize(3).extracting("firstname").contains(flynn.getFirstname(), skyler.getFirstname(), walter.getFirstname()); } @@ -192,7 +190,7 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC Collection collection = personRepository.findPersonDtoBy(); - Assertions.assertThat(collection).hasSize(3).extracting("firstname").contains(flynn.getFirstname(), + assertThat(collection).hasSize(3).extracting("firstname").contains(flynn.getFirstname(), skyler.getFirstname(), walter.getFirstname()); } @@ -320,6 +318,21 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC assertThat(result).contains(walter, skyler, flynn); } + @Test // DATACASS-512 + public void shouldCountRecords() { + + long count = personRepository.countByLastname("White"); + + assertThat(count).isEqualTo(3); + } + + @Test // DATACASS-512 + public void shouldApplyExistsProjection() { + + assertThat(personRepository.existsByLastname("White")).isTrue(); + assertThat(personRepository.existsByLastname("Schrader")).isFalse(); + } + /** * @author Mark Paluch */ @@ -346,6 +359,10 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC Person findByNumberOfChildren(NumberOfChildren numberOfChildren); + long countByLastname(String lastname); + + boolean existsByLastname(String lastname); + Slice findAllSlicedByLastname(String lastname, Pageable pageable); Collection findPersonProjectedBy(); 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 cdebf1ca0..c7449eed3 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 @@ -164,6 +164,26 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac .verifyComplete(); } + @Test // DATACASS-512 + public void shouldCountRecords() { + + StepVerifier.create(repository.countByLastname("Matthews")).expectNext(2L).verifyComplete(); + StepVerifier.create(repository.countByLastname("None")).expectNext(0L).verifyComplete(); + + StepVerifier.create(repository.countQueryByLastname("Matthews")).expectNext(2L).verifyComplete(); + StepVerifier.create(repository.countQueryByLastname("None")).expectNext(0L).verifyComplete(); + } + + @Test // DATACASS-512 + public void shouldApplyExistsProjection() { + + StepVerifier.create(repository.existsByLastname("Matthews")).expectNext(true).verifyComplete(); + StepVerifier.create(repository.existsByLastname("None")).expectNext(false).verifyComplete(); + + StepVerifier.create(repository.existsQueryByLastname("Matthews")).expectNext(true).verifyComplete(); + StepVerifier.create(repository.existsQueryByLastname("None")).expectNext(false).verifyComplete(); + } + interface UserRepository extends ReactiveCassandraRepository { Flux findByLastname(String lastname); @@ -172,8 +192,18 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspac Mono findByLastname(Publisher lastname); + Mono countByLastname(String lastname); + + Mono existsByLastname(String lastname); + @Query("SELECT * FROM users WHERE lastname = ?0") Flux findStringQuery(Mono lastname); + + @CountQuery("SELECT COUNT(*) from users WHERE lastname = ?0") + Mono countQueryByLastname(String lastname); + + @ExistsQuery("SELECT * from users WHERE lastname = ?0") + Mono existsQueryByLastname(String lastname); } interface GroupRepository extends ReactiveCassandraRepository { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java index 0f0ab68e5..ef0b020ea 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java @@ -35,6 +35,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.domain.AllPossibleTypes; +import org.springframework.data.cassandra.repository.CountQuery; +import org.springframework.data.cassandra.repository.ExistsQuery; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; import org.springframework.data.cassandra.repository.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; @@ -269,6 +271,29 @@ public class RepositoryReturnTypesIntegrationTests extends AbstractSpringDataEmb assertThat(result.get("biginteger")).isEqualTo((Object) BigInteger.ONE); } + @Test // DATACASS-512 + public void shouldApplyCountProjection() { + + AllPossibleTypes entity = new AllPossibleTypes("123"); + allPossibleTypesRepository.save(entity); + + assertThat(allPossibleTypesRepository.countById(entity.getId())).isOne(); + assertThat(allPossibleTypesRepository.countById("foo")).isZero(); + } + + @Test // DATACASS-512 + public void shouldApplyExistsProjection() { + + AllPossibleTypes entity = new AllPossibleTypes("123"); + allPossibleTypesRepository.save(entity); + + assertThat(allPossibleTypesRepository.existsUsingCountProjectionById(entity.getId())).isTrue(); + assertThat(allPossibleTypesRepository.existsUsingCountProjectionById("foo")).isFalse(); + + assertThat(allPossibleTypesRepository.existsWithRowsById(entity.getId())).isTrue(); + assertThat(allPossibleTypesRepository.existsWithRowsById("foo")).isFalse(); + } + public interface AllPossibleTypesRepository extends CrudRepository { // blob/byte-buffer result do not work yet. @@ -325,5 +350,14 @@ public class RepositoryReturnTypesIntegrationTests extends AbstractSpringDataEmb @Query("select * from allpossibletypes where id = ?0") Map findEntityAsMapById(String id); + + @CountQuery("select COUNT(*) from allpossibletypes where id = ?0") + long countById(String id); + + @ExistsQuery("select COUNT(*) from allpossibletypes where id = ?0") + boolean existsUsingCountProjectionById(String id); + + @ExistsQuery("select * from allpossibletypes where id = ?0") + boolean existsWithRowsById(String id); } } 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 42b197d0b..2a5c96c12 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 @@ -207,6 +207,22 @@ public class PartTreeCassandraQueryUnitTests { assertThat(statement.getConsistencyLevel()).isEqualTo(ConsistencyLevel.LOCAL_ONE); } + @Test // DATACASS-512 + public void shouldCreateCountQuery() { + + Statement statement = deriveQueryFromMethod(Repo.class, "countBy", new Class[0]); + + assertThat(statement.toString()).isEqualTo("SELECT COUNT(1) FROM person;"); + } + + @Test // DATACASS-512 + public void shouldCreateExistsQuery() { + + Statement statement = deriveQueryFromMethod(Repo.class, "existsBy", new Class[0]); + + assertThat(statement.toString()).isEqualTo("SELECT * FROM person LIMIT 1;"); + } + private String deriveQueryFromMethod(String method, Object... args) { Class[] types = new Class[args.length]; @@ -277,6 +293,10 @@ public class PartTreeCassandraQueryUnitTests { Person findByFirstnameIn(Collection firstname); + long countBy(); + + boolean existsBy(); + @AllowFiltering Person findByFirstname(String firstname); 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 a7bbadd27..425a10271 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 @@ -15,14 +15,15 @@ */ package org.springframework.data.cassandra.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Method; -import java.util.Arrays; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import rx.Single; + +import java.lang.reflect.Method; +import java.util.Arrays; import org.junit.Before; import org.junit.Rule; @@ -31,7 +32,6 @@ 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.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.QueryOptions; @@ -49,8 +49,6 @@ import org.springframework.util.ClassUtils; import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.Statement; -import rx.Single; - /** * Unit tests for {@link ReactivePartTreeCassandraQuery}. * @@ -135,6 +133,22 @@ public class ReactivePartTreeCassandraQueryUnitTests { assertThat(statement.getConsistencyLevel()).isEqualTo(ConsistencyLevel.LOCAL_ONE); } + @Test // DATACASS-512 + public void shouldCreateCountQuery() { + + Statement statement = deriveQueryFromMethod(PartTreeCassandraQueryUnitTests.Repo.class, "countBy", new Class[0]); + + assertThat(statement.toString()).isEqualTo("SELECT COUNT(1) FROM person;"); + } + + @Test // DATACASS-512 + public void shouldCreateExistsQuery() { + + Statement statement = deriveQueryFromMethod(PartTreeCassandraQueryUnitTests.Repo.class, "existsBy", new Class[0]); + + assertThat(statement.toString()).isEqualTo("SELECT * FROM person LIMIT 1;"); + } + private String deriveQueryFromMethod(String method, Object... args) { Class[] types = new Class[args.length]; @@ -186,6 +200,10 @@ public class ReactivePartTreeCassandraQueryUnitTests { Flux findByFirstname(QueryOptions queryOptions, String firstname); + Mono countBy(); + + Mono existsBy(); + @Consistency(ConsistencyLevel.LOCAL_ONE) Flux findPersonBy(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java index 08f6155ad..bcae29e9c 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.cassandra.repository.query; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -24,7 +25,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.cassandra.ReactiveSession; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; @@ -77,6 +77,8 @@ public class ReactiveStringBasedCassandraQueryUnitTests { this.factory = new SpelAwareProxyProjectionFactory(); this.converter.afterPropertiesSet(); + + when(operations.getConverter()).thenReturn(converter); } @Test // DATACASS-335 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 20cc73c0c..e745b4d8a 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; @@ -32,7 +32,6 @@ import org.junit.Test; 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.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.CqlIdentifier; @@ -94,6 +93,8 @@ public class StringBasedCassandraQueryUnitTests { this.factory = new SpelAwareProxyProjectionFactory(); this.converter.afterPropertiesSet(); + + when(operations.getConverter()).thenReturn(converter); } @Test // DATACASS-117 diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 33736ac49..1bdaac352 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -3,6 +3,11 @@ This chapter summarizes changes and new features for each release. +[[new-features.2-1-0]] +== What's new in Spring Data for Apache Cassandra 2.1 +* New annotations for `@CountQuery` and `@ExistsQuery`. +* Template API extended with `count(…)` and `exists(…)` methods accepting `Query`. + [[new-features.2-0-0]] == What's new in Spring Data for Apache Cassandra 2.0