From 43fc7518b31e6ac4e394f649f0ed5a3484517b82 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 1 Jul 2016 18:02:13 +0200 Subject: [PATCH] DATACASS-7 - Support query derivation in Cassandra repositories. We now support query derivation in Cassandra repositories. Repositories may declare query methods and queries are created based on the repository declaration. interface PersonRepository extends CassandraRepository { List findByLastname(@CassandraType(type = Name.VARCHAR) String lastname); List findByLastname(String lastname, Sort sort); List findByLastnameOrderByFirstnameAsc(String lastname); Collection findPersonProjectedBy(); interface PersonProjection { String getFirstname(); } } @Table @Data public class Person { @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) private String lastname; @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED, ordinal = 1) private String firstname; } Query derivation supports a basic set of where predicates: * = (Equals/Simple property) * >= (Greater or equal) * > (Greater) * < (Less) * <= (Less or equal) * IN, LIKE (Like, Starting with, Ending with), CONTAINING * = true (Is true) * = false (Is false) Derived queries work with primary-key and non-primary key columns. Non-primary key columns require a secondary index otherwise these fields can't be queried. Original pull request: #74. --- .../convert/AbstractCassandraConverter.java | 8 + .../cassandra/convert/CassandraConverter.java | 11 +- ...andraPersistentEntityMetadataVerifier.java | 4 + .../BasicCassandraPersistentProperty.java | 3 +- .../query/AbstractCassandraQuery.java | 70 ++- .../query/CassandraParameterAccessor.java | 13 +- .../repository/query/CassandraParameters.java | 17 +- .../CassandraParametersParameterAccessor.java | 29 +- .../query/CassandraQueryCreator.java | 308 ++++++++----- .../query/CassandraQueryMethod.java | 72 ++-- .../query/ConvertingParameterAccessor.java | 159 ++++++- .../query/PartTreeCassandraQuery.java | 23 +- .../query/SimpleCassandraEntityMetadata.java | 64 +++ .../support/CassandraRepositoryFactory.java | 27 +- .../data/cassandra/domain/Person.java | 1 + .../query/CassandraParametersUnitTests.java | 10 +- .../query/CassandraQueryCreatorUnitTests.java | 408 ++++++++++++++++++ .../query/CassandraQueryMethodUnitTests.java | 95 ++++ .../ConvertingParameterAccessorUnitTests.java | 88 +++- .../PartTreeCassandraQueryUnitTests.java | 168 ++++++++ .../query/StubParameterAccessor.java | 102 +++++ .../CassandraRepositoryFactoryUnitTests.java | 89 ++++ ...nousCassandraTemplateIntegrationTests.java | 2 +- .../CassandraOperationsIntegrationTests.java | 6 +- .../querymethods/conversion/Address.java | 32 ++ .../querymethods/conversion/Contact.java | 42 ++ .../ParameterConversionIntegrationTests.java | 178 ++++++++ .../querymethods/declared/Person.java | 9 +- .../derived/PersonRepository.java | 63 +++ .../QueryDerivationIntegrationTests.java | 237 ++++++++++ ...gDataEmbeddedCassandraIntegrationTest.java | 5 + .../integration/support/CassandraVersion.java | 49 +++ ...itoryXmlConfigIntegrationTests-context.xml | 21 +- 33 files changed, 2183 insertions(+), 230 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java index 296b33055..eb3dbb7ad 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java @@ -62,6 +62,14 @@ public abstract class AbstractCassandraConverter implements CassandraConverter, this.conversions = conversions; } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraConverter#getCustomConversions() + */ + @Override + public CustomConversions getCustomConversions() { + return conversions; + } + /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java index 9c73cb7c1..ec2e9a13c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java @@ -46,7 +46,7 @@ public interface CassandraConverter *
  • A the composite primary key for {@link org.springframework.data.cassandra.mapping.PrimaryKey} using a * {@link org.springframework.data.cassandra.mapping.PrimaryKeyClass}
  • * - * + * * @param object must not be {@literal null}. * @param entity must not be {@literal null}. * @return @@ -55,10 +55,17 @@ public interface CassandraConverter /** * Converts and writes a {@code source} object into a {@code sink} using the given {@link CassandraPersistentEntity}. - * + * * @param source the source, may be {@literal null}. * @param sink must not be {@literal null}. * @param entity must not be {@literal null}. */ void write(Object source, Object sink, CassandraPersistentEntity entity); + + /** + * Returns the {@link CustomConversions} registered in the {@link CassandraConverter}. + * + * @return the {@link CustomConversions}. + */ + CustomConversions getCustomConversions(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java index 9f129a84b..204ca8ba9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java @@ -42,6 +42,10 @@ public class BasicCassandraPersistentEntityMetadataVerifier implements Cassandra @Override public void verify(CassandraPersistentEntity entity) throws MappingException { + if(entity.getType().isInterface()){ + return; + } + VerifierMappingExceptions exceptions = new VerifierMappingExceptions(entity, String.format("Mapping Exceptions from BasicCassandraPersistentEntityMetadataVerifier for %s", entity.getName())); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java index 387b4bead..274d1ab03 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java @@ -51,6 +51,7 @@ import com.datastax.driver.core.DataType; * @author Alex Shvid * @author Matthew T. Adams * @author Antoine Toulme + * @author Mark Paluch */ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentProperty implements CassandraPersistentProperty, ApplicationContextAware { @@ -129,7 +130,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP List columnNames = getColumnNames(); if (columnNames.size() != 1) { - throw new IllegalStateException("property does not have a single column mapping"); + throw new IllegalStateException(String.format("Property [%s] has no single column mapping", getName())); } return columnNames.get(0); 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 5bcb81e85..4d251d232 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 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -28,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.CollectionExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingConverter; @@ -38,6 +40,7 @@ import org.springframework.data.cassandra.repository.query.CassandraQueryExecuti import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -65,22 +68,29 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { */ public AbstractCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { - Assert.notNull(operations); - Assert.notNull(method); + Assert.notNull(method, "CassandraQueryMethod must not be null"); + Assert.notNull(operations, "CassandraOperations must not be null"); this.method = method; this.template = operations; } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ @Override public CassandraQueryMethod getQueryMethod() { return method; } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ @Override public Object execute(Object[] parameters) { - CassandraParameterAccessor accessor = new ConvertingParameterAccessor(template.getConverter(), new CassandraParametersParameterAccessor(method, parameters)); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(template.getConverter(), + new CassandraParametersParameterAccessor(method, parameters)); String query = createQuery(accessor); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); @@ -88,7 +98,13 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { CassandraQueryExecution cassandraQueryExecution = getExecution(query, accessor, new ResultProcessingConverter(processor)); - return cassandraQueryExecution.execute(query, processor.getReturnedType().getReturnedType()); + CassandraReturnedType returnedType = new CassandraReturnedType(processor.getReturnedType(), template.getConverter().getCustomConversions()); + + if (returnedType.isProjecting()) { + return cassandraQueryExecution.execute(query, returnedType.getDomainType()); + } + + return cassandraQueryExecution.execute(query, returnedType.getReturnedType()); } /** @@ -167,6 +183,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { return object; } + @Deprecated protected void warnIfMoreResults(Iterator iterator) { if (log.isWarnEnabled() && iterator.hasNext()) { @@ -180,6 +197,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { } } + @Deprecated public ConversionService getConversionService() { return template.getConverter().getConversionService(); } @@ -200,4 +218,48 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { * @param accessor must not be {@literal null}. */ protected abstract String createQuery(CassandraParameterAccessor accessor); + + private class CassandraReturnedType { + + private final ReturnedType returnedType; + private final CustomConversions customConversions; + + CassandraReturnedType(ReturnedType returnedType, CustomConversions customConversions) { + this.returnedType = returnedType; + this.customConversions = customConversions; + } + + boolean isProjecting(){ + + if(!returnedType.isProjecting()){ + return false; + } + + // Spring Data Cassandra allows List and Map declarations on query methods + // so we don't want to let projection kick in + if(ClassUtils.isAssignable(Map.class, returnedType.getReturnedType())){ + return false; + } + + // Type conversion using registered conversions is handled on template level + if(customConversions.hasCustomWriteTarget(returnedType.getReturnedType())){ + return false; + } + + // Don't apply projection on Cassandra simple types + if(customConversions.isSimpleType(returnedType.getReturnedType())){ + return false; + } + + return true; + } + + Class getReturnedType() { + return returnedType.getReturnedType(); + } + + Class getDomainType() { + return returnedType.getDomainType(); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java index 1575e2858..745c4106c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.cassandra.repository.query; +import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.repository.query.ParameterAccessor; import com.datastax.driver.core.DataType; @@ -32,7 +33,7 @@ public interface CassandraParameterAccessor extends ParameterAccessor { * Returns the Cassandra {@link DataType} for the declared parameter if the type is a * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder simple type}. Parameter types may be * specified using {@link org.springframework.data.cassandra.mapping.CassandraType}. - * + * * @param index the parameter index * @return the Cassandra {@link DataType} or {@literal null} if the parameter type cannot be determined from * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder} @@ -41,6 +42,16 @@ public interface CassandraParameterAccessor extends ParameterAccessor { */ DataType getDataType(int index); + /** + * Returns the {@link CassandraType} for the declared method parameter. + * + * @param index the parameter index + * @return the Cassandra {@link CassandraType} or {@literal null}. + * @see org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder + * @see org.springframework.data.cassandra.mapping.CassandraType + */ + CassandraType findCassandraType(int index); + /** * The actual parameter type (after unwrapping). * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java index e08dfedb1..dc9048711 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java @@ -71,7 +71,7 @@ public class CassandraParameters extends Parameters { - private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); - private final CassandraParameterAccessor accessor; + private static final Pattern PUNCTATION_PATTERN = Pattern.compile("\\p{Punct}"); + private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); private final CassandraMappingContext context; + private final WhereBuilder whereBuilder = new WhereBuilder(); + private final CassandraPersistentEntity entity; /** * Creates a new {@link CassandraQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} * and {@link MappingContext}. * - * @param tree - * @param accessor - * @param context + * @param tree must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityMetadata must not be {@literal null}. */ - public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, CassandraMappingContext context) { + public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, CassandraMappingContext context, + EntityMetadata entityMetadata) { super(tree, accessor); - Assert.notNull(context); + Assert.notNull(context, "CassandraMappingContext must not be null"); + Assert.notNull(entityMetadata, "EntityInformation must not be null"); - this.accessor = accessor; this.context = context; + this.entity = context.getPersistentEntity(entityMetadata.getJavaType()); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator) + */ @Override protected Clause create(Part part, Iterator iterator) { PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CassandraPersistentProperty property = path.getLeafProperty(); - Clause criteria = from(part, property, - null /* TODO where(path.toDotPath(CassandraPersistentProperty.PropertyToFieldNameConverter.INSTANCE))*/, - iterator); - - return criteria; + return from(part, property, (PotentiallyConvertingIterator) iterator); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator) + */ @Override protected Clause and(Part part, Clause base, Iterator iterator) { if (base == null) { - return create(part, iterator); + return whereBuilder.and(create(part, iterator)); } - PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); - CassandraPersistentProperty property = path.getLeafProperty(); - - return from(part, property, - null /* TODO base.and(path.toDotPath(CassandraPersistentProperty.PropertyToFieldNameConverter.INSTANCE))*/, - iterator); + whereBuilder.and(base); + return create(part, iterator); } + /* + * Cassandra does not support OR queries. + * + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object) + */ @Override protected Clause or(Clause base, Clause criteria) { throw new InvalidDataAccessApiUsageException(String.format("Cassandra does not support an OR operator!")); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort) + */ @Override protected Select complete(Clause criteria, Sort sort) { - if (criteria == null) { - return null; + if (criteria != null) { + whereBuilder.and(criteria); } - Select select = QueryBuilder.select().all().from("TODO"); - select.where(criteria); + Select select = StatementBuilder.select(entity, whereBuilder, sort); if (LOG.isDebugEnabled()) { - LOG.debug("Created query " + select.getQueryString()); + LOG.debug("Created query {}", select); } return select; } - private Clause from(Part part, CassandraPersistentProperty property, Clause criteria, Iterator parameters) { + private Clause from(Part part, CassandraPersistentProperty property, PotentiallyConvertingIterator parameters) { Type type = part.getType(); switch (type) { - // TODO - // case AFTER: - // case GREATER_THAN: - // return criteria.gt(parameters.nextConverted(property)); - // case GREATER_THAN_EQUAL: - // return criteria.gte(parameters.nextConverted(property)); - // case BEFORE: - // case LESS_THAN: - // return criteria.lt(parameters.nextConverted(property)); - // case LESS_THAN_EQUAL: - // return criteria.lte(parameters.nextConverted(property)); - // case BETWEEN: - // return criteria.gt(parameters.nextConverted(property)).lt(parameters.nextConverted(property)); - // case IS_NOT_NULL: - // return criteria.ne(null); - // case IS_NULL: - // return criteria.is(null); - // case NOT_IN: - // return criteria.nin(nextAsArray(parameters, property)); - // case IN: - // return criteria.in(nextAsArray(parameters, property)); - // case LIKE: - // case STARTING_WITH: - // case ENDING_WITH: - // case CONTAINING: - // return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString()); - // case REGEX: - // return criteria.regex(parameters.next().toString()); - // case EXISTS: - // return criteria.exists((Boolean) parameters.next()); - // case TRUE: - // return criteria.is(true); - // case FALSE: - // return criteria.is(false); - // case WITHIN: - // - // Object parameter = parameters.next(); - // return criteria.within((Shape) parameter); - // case SIMPLE_PROPERTY: - // - // return isSimpleComparisionPossible(part) ? criteria.is(parameters.nextConverted(property)) - // : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, false); - // - // case NEGATING_SIMPLE_PROPERTY: - // - // return isSimpleComparisionPossible(part) ? criteria.ne(parameters.nextConverted(property)) - // : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, true); + case AFTER: + case GREATER_THAN: + return QueryBuilder.gt(columnName(property), parameters.nextConverted(property)); + case GREATER_THAN_EQUAL: + return QueryBuilder.gte(columnName(property), parameters.nextConverted(property)); + case BEFORE: + case LESS_THAN: + return QueryBuilder.lt(columnName(property), parameters.nextConverted(property)); + case LESS_THAN_EQUAL: + return QueryBuilder.lte(columnName(property), parameters.nextConverted(property)); + case IN: + return QueryBuilder.in(columnName(property), nextAsArray(property, parameters)); + case LIKE: + case STARTING_WITH: + case ENDING_WITH: + return QueryBuilder.like(columnName(property), like(type, parameters.nextConverted(property))); + case CONTAINING: + return containing(property, parameters.nextConverted(property)); + case TRUE: + return QueryBuilder.eq(columnName(property), true); + case FALSE: + return QueryBuilder.eq(columnName(property), false); + case SIMPLE_PROPERTY: + return QueryBuilder.eq(columnName(property), parameters.nextConverted(property)); default: - throw new UnsupportedCassandraQueryOperatorException(String.format("")); + throw new InvalidDataAccessApiUsageException( + String.format("Unsupported Keyword: [%s] in part [%s]", type, part)); } } - private boolean isSimpleComparisionPossible(Part part) { + private Clause containing(CassandraPersistentProperty property, Object bindableValue) { - switch (part.shouldIgnoreCase()) { - case NEVER: - return true; - case WHEN_POSSIBLE: - return part.getProperty().getType() != String.class; - case ALWAYS: - return false; - default: - return true; - } - } - - /** - * Returns the next element from the given {@link Iterator} expecting it to be of a certain type. - * - * @param - * @param iterator - * @param type - * @throws IllegalArgumentException in case the next element in the iterator is not of the given type. - * @return - */ - @SuppressWarnings("unchecked") - private T nextAs(Iterator iterator, Class type) { - Object parameter = iterator.next(); - if (parameter.getClass().isAssignableFrom(type)) { - return (T) parameter; + if (property.isCollectionLike() || ClassUtils.isAssignable(Map.class, property.getType())) { + return QueryBuilder.contains(columnName(property), bindableValue); } - throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type, - parameter.getClass())); + return QueryBuilder.like(columnName(property), like(Type.CONTAINING, bindableValue)); } - private Object[] nextAsArray(Iterator iterator, CassandraPersistentProperty property) { - Object next = iterator.next(); // TODO nextConverted(property); + private Object like(Type type, Object value) { + + if (value == null) { + return null; + } + + if (type == Type.LIKE) { + return value; + } + + if (type == Type.CONTAINING) { + return "%" + value + "%"; + } + + if (type == Type.STARTING_WITH) { + return value + "%"; + } + + if (type == Type.ENDING_WITH) { + return "%" + value; + } + + throw new IllegalArgumentException(String.format("Part Type [%s] not supported with like queries", type)); + + } + + private static String columnName(CassandraPersistentProperty property) { + return property.getColumnName().toCql(); + } + + private Object[] nextAsArray(CassandraPersistentProperty property, PotentiallyConvertingIterator iterator) { + + Object next = iterator.nextConverted(property); if (next instanceof Collection) { return ((Collection) next).toArray(); @@ -217,4 +222,91 @@ class CassandraQueryCreator extends AbstractQueryCreator { return new Object[] { next }; } + + /** + * Where clause builder. Collects {@link Clause clauses} and builds the where-clause depending on the WHERE type. + * + * @author Mark Paluch + */ + static class WhereBuilder { + + private List clauses = new ArrayList(); + + Clause and(Clause clause) { + + clauses.add(clause); + return clause; + } + + Select.Where build(Select.Where where) { + + for (Clause clause : clauses) { + where = where.and(clause); + } + return where; + } + } + + /** + * @author Mark Paluch + */ + static class StatementBuilder { + + /** + * Build a {@link Select} statement from the given {@link WhereBuilder} and {@link Sort}. Resolves property names + * for {@link Sort} using the {@link CassandraPersistentEntity}. + * + * @param whereBuilder + * @param entity + * @param sort + * @return + */ + static Select select(CassandraPersistentEntity entity, WhereBuilder whereBuilder, Sort sort) { + + Select select = QueryBuilder.select().from(entity.getTableName().toCql()); + whereBuilder.build(select.where()); + + if (sort != null) { + + for (Order order : sort) { + + String dotPath = order.getProperty(); + CassandraPersistentProperty property = getPersistentProperty(entity, dotPath); + + if (order.isAscending()) { + select.orderBy(QueryBuilder.asc(columnName(property))); + } else { + select.orderBy(QueryBuilder.desc(columnName(property))); + } + } + } + + return select; + } + + private static CassandraPersistentProperty getPersistentProperty(CassandraPersistentEntity entity, + String dotPath) { + + String[] segments = PUNCTATION_PATTERN.split(dotPath); + + CassandraPersistentProperty property = null; + CassandraPersistentEntity currentEntity = entity; + + for (String segment : segments) { + property = currentEntity.getPersistentProperty(segment); + + if (property != null && property.isCompositePrimaryKey()) { + currentEntity = property.getCompositePrimaryKeyEntity(); + } + } + + if (property != null) { + + return property; + } + + throw new IllegalArgumentException( + String.format("Cannot resolve path [%s] to a property of [%s]", dotPath, entity.getName())); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index be295f771..b38f07a57 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -21,6 +21,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -28,6 +29,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.datastax.driver.core.ResultSet; @@ -41,16 +43,9 @@ import com.datastax.driver.core.ResultSet; */ public class CassandraQueryMethod extends QueryMethod { - private boolean queryCached = false; - - @SuppressWarnings("all") - private final CassandraMappingContext mappingContext; - private final Method method; - - private Query query; - - private String queryString; + private final CassandraMappingContext mappingContext; + private CassandraEntityMetadata metadata; /** * Creates a new {@link CassandraQueryMethod} from the given {@link Method}. @@ -85,6 +80,36 @@ public class CassandraQueryMethod extends QueryMethod { } } + @Override + public CassandraEntityMetadata getEntityInformation() { + + if (metadata == null) { + + Class returnedObjectType = getReturnedObjectType(); + Class domainClass = getDomainClass(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) { + + this.metadata = new SimpleCassandraEntityMetadata((Class) domainClass, + mappingContext.getPersistentEntity(domainClass)); + + } else { + + CassandraPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); + CassandraPersistentEntity managedEntity = mappingContext.getPersistentEntity(domainClass); + returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity + : returnedEntity; + CassandraPersistentEntity collectionEntity = domainClass.isAssignableFrom(returnedObjectType) + ? returnedEntity : managedEntity; + + this.metadata = new SimpleCassandraEntityMetadata((Class) returnedEntity.getType(), + collectionEntity); + } + } + + return this.metadata; + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) */ @@ -93,18 +118,6 @@ public class CassandraQueryMethod extends QueryMethod { return new CassandraParameters(method); } - /** - * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. - */ - Query getQueryAnnotation() { - if (query == null) { - query = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); - queryCached = true; - } - - return query; - } - /** * Returns whether the method has an annotated query. */ @@ -115,15 +128,22 @@ public class CassandraQueryMethod extends QueryMethod { /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. + * + * @return */ public String getAnnotatedQuery() { - if (!queryCached) { - queryString = (String) AnnotationUtils.getValue(getQueryAnnotation()); - queryString = (StringUtils.hasText(queryString) ? queryString : null); - } + String query = (String) AnnotationUtils.getValue(getQueryAnnotation()); + return StringUtils.hasText(query) ? query : null; + } - return queryString; + /** + * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. + * + * @return + */ + Query getQueryAnnotation() { + return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); } /** diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java index 17c0968b6..a288019e5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java @@ -15,14 +15,26 @@ */ package org.springframework.data.cassandra.repository.query; +import java.util.Collection; import java.util.Iterator; +import java.util.Set; +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; +import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.CollectionType; import com.datastax.driver.core.TypeCodec; /** @@ -34,10 +46,12 @@ import com.datastax.driver.core.TypeCodec; */ class ConvertingParameterAccessor implements CassandraParameterAccessor { + private final static TypeInformation SET = ClassTypeInformation.from(Set.class); + private final CassandraConverter cassandraConverter; private final CassandraParameterAccessor delegate; - public ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { + ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { this.cassandraConverter = cassandraConverter; this.delegate = delegate; @@ -72,7 +86,12 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { */ @Override public Object getBindableValue(int index) { - return potentiallyConvert(index, delegate.getBindableValue(index)); + return potentiallyConvert(index, delegate.getBindableValue(index), null); + } + + @Override + public CassandraType findCassandraType(int index) { + return delegate.findCassandraType(index); } /* (non-Javadoc) @@ -80,7 +99,14 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { */ @Override public DataType getDataType(int index) { - return delegate.getDataType(index); + + DataType dataType = delegate.getDataType(index); + + if (dataType != null) { + return dataType; + } + + return cassandraConverter.getMappingContext().getDataType(getParameterType(index)); } /* (non-Javadoc) @@ -99,11 +125,6 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return delegate.hasBindableNullValue(); } - /* - * (non-Javadoc) - * - * @see java.lang.Iterable#iterator() - */ /* (non-Javadoc) * @see org.springframework.data.repository.query.ParameterAccessor#iterator() */ @@ -111,20 +132,37 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return new ConvertingIterator(delegate.iterator()); } - private Object potentiallyConvert(int index, Object bindableValue) { + @SuppressWarnings("unchecked") + private Object potentiallyConvert(int index, Object bindableValue, CassandraPersistentProperty property) { if (bindableValue == null) { return null; } - DataType parameterType = getDataType(index); - - if (parameterType == null) { - parameterType = cassandraConverter.getMappingContext().getDataType(getParameterType(index)); + if (bindableValue.getClass().isArray()) { + return bindableValue; } + DataType parameterType = getDataType(index, property); TypeCodec cassandraType = CodecRegistry.DEFAULT_INSTANCE.codecFor(parameterType); + if (property != null && getCustomConversions().hasCustomWriteTarget(property.getActualType()) + && property.isCollectionLike()) { + + Class customWriteTarget = getCustomConversions().getCustomWriteTarget(property.getActualType()); + if (Collection.class.isAssignableFrom(property.getType()) && bindableValue instanceof Collection) { + + Collection original = (Collection) bindableValue; + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object o : original) { + converted.add(getConversionService().convert(o, customWriteTarget)); + } + + return converted; + } + } + if (cassandraType.getJavaType().getRawType().isAssignableFrom(bindableValue.getClass())) { return bindableValue; } @@ -132,12 +170,79 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return cassandraConverter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); } + private CustomConversions getCustomConversions() { + return cassandraConverter.getCustomConversions(); + } + + private ConversionService getConversionService() { + return cassandraConverter.getConversionService(); + } + + /** + * Return the {@link DataType} based on annotated parameters with {@link CassandraType}, the + * {@link CassandraPersistentProperty} type or the declared parameter type. + * + * @param index + * @param cassandraPersistentProperty + * @return the {@link DataType} + */ + DataType getDataType(int index, CassandraPersistentProperty cassandraPersistentProperty) { + + CassandraType cassandraType = delegate.findCassandraType(index); + + if (cassandraType != null) { + return CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type()); + } + + CassandraMappingContext mappingContext = cassandraConverter.getMappingContext(); + TypeInformation typeInformation = ClassTypeInformation.from(getParameterType(index)); + + if (cassandraPersistentProperty == null) { + return mappingContext.getDataType(typeInformation.getType()); + } + + DataType dataType = mappingContext.getDataType(cassandraPersistentProperty); + + if (cassandraPersistentProperty.isCollectionLike() && !typeInformation.isCollectionLike()) { + + if (dataType instanceof CollectionType) { + + CollectionType collectionType = (CollectionType) dataType; + if (collectionType.getTypeArguments().size() == 1) { + return collectionType.getTypeArguments().get(0); + } + } + } + + if (!cassandraPersistentProperty.isCollectionLike() && typeInformation.isCollectionLike()) { + + if (typeInformation.isAssignableFrom(SET)) { + return DataType.set(dataType); + } + + return DataType.list(dataType); + } + + if (cassandraPersistentProperty.isMap()) { + + if (dataType instanceof CollectionType) { + + CollectionType collectionType = (CollectionType) dataType; + if (collectionType.getTypeArguments().size() == 2) { + return collectionType.getTypeArguments().get(0); + } + } + } + + return mappingContext.getDataType(cassandraPersistentProperty); + } + /** * Custom {@link Iterator} to convert items before returning them. * * @author Mark Paluch */ - private class ConvertingIterator implements Iterator { + private class ConvertingIterator implements PotentiallyConvertingIterator { private final Iterator delegate; private int index = 0; @@ -145,9 +250,9 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { /** * Creates a new {@link ConvertingIterator} for the given delegate. * - * @param delegate + * @param delegate must not be {@literal null}. */ - public ConvertingIterator(Iterator delegate) { + ConvertingIterator(Iterator delegate) { this.delegate = delegate; } @@ -164,7 +269,7 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { * @see java.util.Iterator#next() */ public Object next() { - return potentiallyConvert(index++, next()); + return potentiallyConvert(index++, delegate.next(), null); } /* @@ -174,5 +279,25 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { public void remove() { delegate.remove(); } + + @Override + public Object nextConverted(CassandraPersistentProperty property) { + return potentiallyConvert(index++, delegate.next(), property); + } + } + + /** + * Custom {@link Iterator} that adds a method to access elements in a converted manner. + * + * @author Mark Paluch + */ + interface PotentiallyConvertingIterator extends Iterator { + + /** + * Returns the next element and pass in type information for potential conversion. + * + * @return the converted object, may be {@literal null}. + */ + Object nextConverted(CassandraPersistentProperty property); } } 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 9b4439951..f1106a52d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2014-2016 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. @@ -24,6 +24,9 @@ import org.springframework.data.repository.query.parser.PartTree; /** * {@link RepositoryQuery} implementation for Cassandra. + * + * @author Matthew Adams + * @author Mark Paluch */ public class PartTreeCassandraQuery extends AbstractCassandraQuery { @@ -34,13 +37,14 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { * Creates a new {@link PartTreeCassandraQuery} from the given {@link QueryMethod} and {@link CassandraTemplate}. * * @param method must not be {@literal null}. - * @param template must not be {@literal null}. + * @param operations must not be {@literal null}. */ - public PartTreeCassandraQuery(CassandraQueryMethod method, CassandraOperations cassandraOperations) { + public PartTreeCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { + + super(method, operations); - super(method, cassandraOperations); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); - this.context = cassandraOperations.getConverter().getMappingContext(); + this.context = operations.getConverter().getMappingContext(); } /** @@ -52,10 +56,15 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { return tree; } + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean) + */ @Override protected String createQuery(CassandraParameterAccessor accessor) { - CassandraQueryCreator creator = new CassandraQueryCreator(tree, accessor, context); - return creator.createQuery().getQueryString(); + CassandraQueryCreator creator = new CassandraQueryCreator(tree, accessor, context, + getQueryMethod().getEntityInformation()); + return creator.createQuery().toString(); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java new file mode 100644 index 000000000..906714590 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 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 org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.util.Assert; + +/** + * Implementation of {@link CassandraEntityMetadata} based on the type and {@link CassandraPersistentEntity}. + * + * @author Mark Paluch + * @since 1.5 + */ +class SimpleCassandraEntityMetadata implements CassandraEntityMetadata { + + private final Class type; + private final CassandraPersistentEntity tableEntity; + + /** + * Creates a new {@link SimpleCassandraEntityMetadata} using the given type and {@link CassandraPersistentEntity} to + * use for table lookups. + * + * @param type must not be {@literal null}. + * @param tableEntity must not be {@literal null} or empty. + */ + public SimpleCassandraEntityMetadata(Class type, CassandraPersistentEntity tableEntity) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(tableEntity, "Collection entity must not be null or empty!"); + + this.type = type; + this.tableEntity = tableEntity; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraEntityMetadata#getTableName() + */ + @Override + public CqlIdentifier getTableName() { + return tableEntity.getTableName(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.EntityMetadata#getJavaType() + */ + @Override + public Class getJavaType() { + return type; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java index 15959297c..bf9a7e538 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors + * Copyright 2013-2016 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. @@ -18,13 +18,13 @@ package org.springframework.data.cassandra.repository.support; import java.io.Serializable; import java.lang.reflect.Method; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; import org.springframework.data.cassandra.repository.query.CassandraQueryMethod; +import org.springframework.data.cassandra.repository.query.PartTreeCassandraQuery; import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.projection.ProjectionFactory; @@ -43,8 +43,8 @@ import org.springframework.util.Assert; * @author Alex Shvid * @author Matthew T. Adams * @author Thomas Darimont + * @author Mark Paluch */ - public class CassandraRepositoryFactory extends RepositoryFactorySupport { private final CassandraOperations cassandraOperations; @@ -61,16 +61,19 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { this.cassandraOperations = cassandraOperations; this.mappingContext = cassandraOperations.getConverter().getMappingContext(); - - // TODO: remove when supporting declarative query methods - setQueryLookupStrategyKey(QueryLookupStrategy.Key.USE_DECLARED_QUERY); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) + */ @Override protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleCassandraRepository.class; } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation) + */ @Override protected Object getTargetRepository(RepositoryInformation information) { @@ -78,6 +81,9 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { return getTargetRepositoryViaReflection(information, entityInformation, cassandraOperations); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) + */ @Override @SuppressWarnings("unchecked") public CassandraEntityInformation getEntityInformation(Class domainClass) { @@ -85,14 +91,17 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { CassandraPersistentEntity entity = mappingContext.getPersistentEntity(domainClass); if (entity == null) { - throw new MappingException(String.format("Could not lookup mapping metadata for domain class %s!", - domainClass.getName())); + throw new MappingException( + String.format("Could not lookup mapping metadata for domain class %s!", domainClass.getName())); } return new MappingCassandraEntityInformation((CassandraPersistentEntity) entity, cassandraOperations.getConverter()); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key) + */ @Override protected QueryLookupStrategy getQueryLookupStrategy(Key key) { return new CassandraQueryLookupStrategy(); @@ -117,7 +126,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { } else if (queryMethod.hasAnnotatedQuery()) { return new StringBasedCassandraQuery(queryMethod, cassandraOperations); } else { - throw new InvalidDataAccessApiUsageException("declarative query methods are a todo"); + return new PartTreeCassandraQuery(queryMethod, cassandraOperations); } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java index 92eccf73f..706ec31f5 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java @@ -29,4 +29,5 @@ public class Person { @Id String id; String firstname; + String lastname; } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java index 965eeed6a..aea5da220 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java @@ -44,12 +44,12 @@ public class CassandraParametersUnitTests { * @see DATACASS-296 */ @Test - public void shouldReturnDataTypeForSimpleType() throws Exception { + public void shouldUnknownDataTypeForSimpleType() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.varchar())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(nullValue())); } /** @@ -61,7 +61,8 @@ public class CassandraParametersUnitTests { Method method = PersonRepository.class.getMethod("findByFirstTime", String.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(notNullValue())); + assertThat(cassandraParameters.getParameter(0).getCassandraType().type(), is(Name.TIME)); } /** @@ -85,7 +86,8 @@ public class CassandraParametersUnitTests { Method method = PersonRepository.class.getMethod("findByAnnotatedObject", Object.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(notNullValue())); + assertThat(cassandraParameters.getParameter(0).getCassandraType().type(), is(Name.TIME)); } interface PersonRepository { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java new file mode 100644 index 000000000..906f7a4ce --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java @@ -0,0 +1,408 @@ +/* + * Copyright 2016 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.cassandra.repository.query.StubParameterAccessor.*; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.cassandra.mapping.PrimaryKey; +import org.springframework.data.cassandra.mapping.PrimaryKeyClass; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Unit tests for {@link CassandraQueryCreator}. + * + * @author Mark Paluch + * @soundtrack Odyssey - Everybody Move 9Club Mix + */ +public class CassandraQueryCreatorUnitTests { + + CassandraMappingContext context; + CassandraConverter converter; + + @Rule public ExpectedException expection = ExpectedException.none(); + + @Before + public void setUp() throws SecurityException, NoSuchMethodException { + + context = new BasicCassandraMappingContext(); + converter = new MappingCassandraConverter(context); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryCorrectly() { + + String query = createQuery("findByFirstname", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryWithSortCorrectly() { + + String query = createQuery("findByFirstnameOrderByLastname", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter' ORDER BY lastname ASC;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsAndQueryCorrectly() { + + String query = createQuery("findByFirstnameAndLastname", Person.class, "Walter", "White"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter' AND lastname='White';"))); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void rejectsNegatingQueryQuery() { + createQuery("findByFirstnameNot", Person.class, "Walter"); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void rejectsOrQuery() { + createQuery("findByFirstnameOrLastname", Person.class, "Walter", "White"); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsGreaterThanQueryCorrectly() { + + String query = createQuery("findByFirstnameGreaterThan", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname>'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsGreaterThanEqualQueryCorrectly() { + + String query = createQuery("findByFirstnameGreaterThanEqual", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname>='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLessThanQueryCorrectly() { + + String query = createQuery("findByFirstnameLessThan", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname<'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLessThanEqualQueryCorrectly() { + + String query = createQuery("findByFirstnameLessThanEqual", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname<='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryCorrectly() { + + String query = createQuery("findByFirstnameIn", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter');"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryWithListCorrectly() { + + String query = createQuery("findByFirstnameIn", Person.class, Arrays.asList("Walter", "Gus")); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter','Gus');"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryWithArrayCorrectly() { + + String query = createQuery("findByFirstnameInAndLastname", Person.class, new String[] { "Walter", "Gus" }, "Fring"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter','Gus') AND lastname='Fring';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLikeQueryCorrectly() { + + assertThat(createQuery("findByFirstnameLike", Person.class, "Wal%ter"), + is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Wal%ter';"))); + assertThat(createQuery("findByFirstnameLike", Person.class, "Walter"), + is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsStartsWithQueryCorrectly() { + + String query = createQuery("findByFirstnameStartsWith", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Walter%';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsEndsWithQueryCorrectly() { + + String query = createQuery("findByFirstnameEndsWith", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE '%Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnSimplePropertyCorrectly() { + + String query = createQuery("findByFirstnameContains", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE '%Walter%';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnSetPropertyCorrectly() { + + String query = createQuery("findByMysetContains", TypeWithSet.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithset WHERE myset CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnListPropertyCorrectly() { + + String query = createQuery("findByMylistContains", TypeWithList.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithlist WHERE mylist CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnMapPropertyCorrectly() { + + String query = createQuery("findByMymapContains", TypeWithMap.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithmap WHERE mymap CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsIsTrueQueryCorrectly() { + + String query = createQuery("findByFirstnameIsTrue", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname=true;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsIsFalseQueryCorrectly() { + + String query = createQuery("findByFirstnameIsFalse", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname=false;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryUsingQuotingCorrectly() { + + String query = createQuery("findByIdAndSet", QuotedType.class, "Walter", "White"); + + assertThat(query, is(equalTo("SELECT * FROM \"myTable\" WHERE \"my_id\"='Walter' AND \"set\"='White';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartCorrectly() { + + String query = createQuery("findByKeyFirstname", TypeWithCompositeId.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithcompositeid WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartWithSortCorrectly() { + + String query = createQuery("findByKeyFirstnameOrderByKeyLastnameAsc", TypeWithCompositeId.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithcompositeid WHERE firstname='Walter' ORDER BY lastname ASC;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartOfPrimaryKeyClassCorrectly() { + + String query = createQuery("findByFirstname", Key.class, "Walter"); + + // ⊙_ʘ rly? ヾ( •́д•̀ ;)ノ + assertThat(query, is(equalTo("SELECT * FROM key WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = IllegalStateException.class) + public void createsFindByPrimaryKey2PartCorrectly() { + createQuery("findByKey", TypeWithCompositeId.class, new Key()); + } + + private String createQuery(String source, Class entityClass, Object... values) { + + PartTree tree = new PartTree(source, entityClass); + CassandraQueryCreator creator = new CassandraQueryCreator(tree, getAccessor(converter, values), context, + getEntityInformation(entityClass)); + return creator.createQuery().toString(); + } + + private EntityMetadata getEntityInformation(final Class entityClass) { + return new EntityMetadata() { + @Override + public Class getJavaType() { + return entityClass; + } + }; + } + + @Table + private static class TypeWithSet { + + @Id String id; + Set myset; + } + + @Table + private static class TypeWithList { + + @Id String id; + List mylist; + } + + @Table + private static class TypeWithMap { + + @Id String id; + Map mymap; + } + + @Table(value = "myTable", forceQuote = true) + private static class QuotedType { + + @PrimaryKey(value = "my_id", forceQuote = true) String id; + + @Column(value = "set") Set set; + } + + @PrimaryKeyClass + private static class Key implements Serializable { + + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1) String firstname; + + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1) String lastname; + } + + @Table + private static class TypeWithCompositeId { + + @PrimaryKey Key key; + + String city; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java new file mode 100644 index 000000000..1378bbc26 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link CassandraQueryMethod}. + * + * @author Mark Paluch + */ +public class CassandraQueryMethodUnitTests { + + CassandraMappingContext context; + + @Before + public void setUp() { + context = new BasicCassandraMappingContext(); + } + + /** + * @see DATACASS-7 + */ + @Test + public void detectsCollectionFromRepoTypeIfReturnTypeNotAssignable() throws Exception { + + CassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); + CassandraEntityMetadata metadata = queryMethod.getEntityInformation(); + + assertThat(metadata.getJavaType(), is(typeCompatibleWith(Person.class))); + assertThat(metadata.getTableName().toCql(), is("person")); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMappingContext() throws Exception { + + Method method = SampleRepository.class.getMethod("method"); + + new CassandraQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + new SpelAwareProxyProjectionFactory(), null); + } + + /** + * @see DATACASS-7 + */ + @Test + public void considersMethodAsCollectionQuery() throws Exception { + + CassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); + + assertThat(queryMethod.isCollectionQuery(), is(true)); + } + + private CassandraQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { + + Method method = repository.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + return new CassandraQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); + } + + interface SampleRepository extends Repository { + + List method(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java index 649cddf53..d4c0c1940 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java @@ -20,6 +20,10 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -28,6 +32,9 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator; import com.datastax.driver.core.DataType; @@ -41,14 +48,16 @@ import com.datastax.driver.core.DataType; public class ConvertingParameterAccessorUnitTests { @Mock CassandraParameterAccessor delegateMock; - + @Mock CassandraPersistentProperty propertyMock; MappingCassandraConverter converter; + ConvertingParameterAccessor accessor; @Before public void setUp() { this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); this.converter.afterPropertiesSet(); + this.accessor = new ConvertingParameterAccessor(converter, delegateMock); } /** @@ -56,9 +65,6 @@ public class ConvertingParameterAccessorUnitTests { */ @Test public void shouldReturnNullBindableValue() { - - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - assertThat(accessor.getBindableValue(0), is(nullValue())); } @@ -66,12 +72,12 @@ public class ConvertingParameterAccessorUnitTests { * @see DATACASS-296 */ @Test + @SuppressWarnings({"rawtypes", "unchecked"}) public void shouldReturnNativeBindableValue() { - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - when(delegateMock.getBindableValue(0)).thenReturn("hello"); when(delegateMock.getDataType(0)).thenReturn(DataType.varchar()); + when(delegateMock.getParameterType(0)).thenReturn((Class) String.class); assertThat(accessor.getBindableValue(0), is(equalTo((Object) "hello"))); } @@ -80,11 +86,9 @@ public class ConvertingParameterAccessorUnitTests { * @see DATACASS-296 */ @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) public void shouldReturnConvertedBindableValue() { - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - LocalDate localDate = LocalDate.of(2010, 7, 4); when(delegateMock.getBindableValue(0)).thenReturn(localDate); @@ -93,4 +97,70 @@ public class ConvertingParameterAccessorUnitTests { assertThat(accessor.getBindableValue(0), is(equalTo((Object) com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 7, 4)))); } + + /** + * @see DATACASS-296 + * @see DATACASS-7 + */ + @Test + public void shouldReturnDataTypeProvidedByDelegate() { + + when(delegateMock.getDataType(0)).thenReturn(DataType.varchar()); + + assertThat(accessor.getDataType(0), is(equalTo(DataType.varchar()))); + } + + /** + * @see DATACASS-296 + * @see DATACASS-7 + */ + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void shouldConvertCollections() { + + LocalDate localDate = LocalDate.of(2010, 7, 4); + + when(delegateMock.iterator()).thenReturn((Iterator) Arrays.asList(Collections.singletonList(localDate)).iterator()); + when(delegateMock.getDataType(0)).thenReturn(DataType.list(DataType.date())); + when(delegateMock.getParameterType(0)).thenReturn((Class) List.class); + when(propertyMock.getType()).thenReturn((Class) List.class); + when(propertyMock.getActualType()).thenReturn((Class) LocalDate.class); + when(propertyMock.isCollectionLike()).thenReturn(true); + + PotentiallyConvertingIterator iterator = (PotentiallyConvertingIterator) accessor.iterator(); + Object converted = iterator.nextConverted(propertyMock); + + assertThat(converted, is(instanceOf(List.class))); + + List list = (List) converted; + assertThat(list.get(0), is(instanceOf(com.datastax.driver.core.LocalDate.class))); + } + + /** + * @see DATACASS-7 + */ + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void shouldProvideTypeBasedOnValue() { + + when(delegateMock.getDataType(0)).thenReturn(null); + when(delegateMock.getParameterType(0)).thenReturn((Class) LocalDate.class); + + assertThat(accessor.getDataType(0), is(equalTo(DataType.date()))); + } + + /** + * @see DATACASS-7 + */ + @Test + @SuppressWarnings("rawtypes") + public void shouldProvideTypeBasedOnPropertyType() { + + when(propertyMock.getDataType()).thenReturn(DataType.varchar()); + when(propertyMock.findAnnotation(CassandraType.class)).thenReturn(mock(CassandraType.class)); + when(delegateMock.getParameterType(0)).thenReturn((Class) String.class); + when(delegateMock.getDataType(0)).thenReturn(null); + + assertThat(accessor.getDataType(0, propertyMock), is(equalTo(DataType.varchar()))); + } } 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 new file mode 100644 index 000000000..2bd5fc542 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2016 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link PartTreeCassandraQuery}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class PartTreeCassandraQueryUnitTests { + + public @Rule ExpectedException exception = ExpectedException.none(); + + @Mock CassandraOperations cassandraOperationsMock; + + CassandraMappingContext mappingContext; + CassandraConverter converter; + + @Before + public void setUp() { + + mappingContext = new BasicCassandraMappingContext(); + converter = new MappingCassandraConverter(mappingContext); + when(cassandraOperationsMock.getConverter()).thenReturn(converter); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveSimpleQuery() { + + String query = deriveQueryFromMethod("findByLastname", "foo"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE lastname='foo';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveSimpleQueryWithoutNames() { + + String query = deriveQueryFromMethod("findPersonBy"); + + assertThat(query, is(equalTo("SELECT * FROM person;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveAndQuery() { + + String query = deriveQueryFromMethod("findByFirstnameAndLastname", "foo", "bar" ); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='foo' AND lastname='bar';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void usesDynamicProjection() { + + String query = deriveQueryFromMethod("findDynamicallyProjectedBy", PersonProjection.class); + + assertThat(query, is(equalTo("SELECT * FROM person;"))); + } + + + private String deriveQueryFromMethod(String method, Object... args) { + + Class[] types = new Class[args.length]; + + for (int i = 0; i < args.length; i++) { + types[i] = args[i].getClass(); + } + + PartTreeCassandraQuery partTreeQuery = createQueryForMethod(method, types); + + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(), args); + return partTreeQuery.createQuery(new ConvertingParameterAccessor(cassandraOperationsMock.getConverter(), accessor)); + } + + private PartTreeCassandraQuery createQueryForMethod(String methodName, Class... paramTypes) { + + try { + + Method method = Repo.class.getMethod(methodName, paramTypes); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory, + mappingContext); + + return new PartTreeCassandraQuery(queryMethod, cassandraOperationsMock); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } catch (SecurityException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + @SuppressWarnings("unused") + interface Repo extends CassandraRepository { + + @Query() + Person findByLastname(String lastname); + + Person findByFirstnameAndLastname(String firstname, String lastname); + + Person findPersonByFirstnameAndLastname(String firstname, String lastname); + + Person findByAge(Integer age); + + Person findPersonBy(); + + PersonProjection findPersonProjectedBy(); + + T findDynamicallyProjectedBy(Class type); + + } + + interface PersonProjection { + + String getFirstname(); + + String getLastname(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java new file mode 100644 index 000000000..75abb515d --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016 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 java.util.Arrays; +import java.util.Iterator; + +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; +import org.springframework.data.repository.query.ParameterAccessor; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; + +/** + * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. + * + * @author Mark Paluch + */ +class StubParameterAccessor implements CassandraParameterAccessor { + + private final Object[] values; + + /** + * Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the + * given parameters converted but unfiltered. + * + * @param converter + * @param parameters + * @return + */ + public static ConvertingParameterAccessor getAccessor(CassandraConverter converter, Object... parameters) { + return new ConvertingParameterAccessor(converter, new StubParameterAccessor(parameters)); + } + + @SuppressWarnings("unchecked") + public StubParameterAccessor(Object... values) { + this.values = values; + } + + @Override + public DataType getDataType(int index) { + return CodecRegistry.DEFAULT_INSTANCE.codecFor(values[index]).getCqlType(); + } + + @Override + public Class getParameterType(int index) { + return values[index].getClass(); + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Override + public Class getDynamicProjection() { + return null; + } + + @Override + public Object getBindableValue(int index) { + return values[index]; + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + public Iterator iterator() { + return Arrays.asList(values).iterator(); + } + + @Override + public CassandraType findCassandraType(int index) { + return null; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java new file mode 100644 index 000000000..29842dc2e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016 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.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.Repository; + +/** + * Unit tests for {@link CassandraRepositoryFactory}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class CassandraRepositoryFactoryUnitTests { + + @Mock CassandraTemplate template; + @Mock CassandraConverter converter; + @Mock CassandraMappingContext mappingContext; + @Mock CassandraPersistentEntity entity; + + @Before + public void setUp() { + + when(template.getConverter()).thenReturn(converter); + when(converter.getMappingContext()).thenReturn(mappingContext); + } + + /** + * @see DATACASS-7 + */ + @Test + public void usesMappingCassandraEntityInformationIfMappingContextSet() { + + when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity); + when(entity.getType()).thenReturn(Person.class); + + CassandraRepositoryFactory factory = new CassandraRepositoryFactory(template); + CassandraEntityInformation entityInformation = factory.getEntityInformation(Person.class); + assertTrue(entityInformation instanceof MappingCassandraEntityInformation); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsRepositoryWithIdTypeLong() { + + when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity); + when(entity.getType()).thenReturn(Person.class); + + CassandraRepositoryFactory factory = new CassandraRepositoryFactory(template); + MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); + assertThat(repository, is(notNullValue())); + } + + interface MyPersonRepository extends Repository { + + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java index 3057a97e4..5dda280cc 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java @@ -246,7 +246,7 @@ public class AsynchronousCassandraTemplateIntegrationTests extends AbstractSprin @AllArgsConstructor @NoArgsConstructor @SuppressWarnings("unused") - public static class Person { + static class Person { @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) String id; @Column String firstname; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java index be1f9170f..aba473d43 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java @@ -734,7 +734,11 @@ public class CassandraOperationsIntegrationTests extends AbstractSpringDataEmbed * @see DATACASS-182 */ @Test - public void stream() { + public void stream() throws InterruptedException { + + while(template.select("SELECT * FROM book", Book.class).size() != 0){ + Thread.sleep(10); + } template.insert(getBookList(20)); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java new file mode 100644 index 000000000..933d97db5 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 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.test.integration.repository.querymethods.conversion; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +class Address { + + String city; + String country; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java new file mode 100644 index 000000000..a646a8c0f --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 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.test.integration.repository.querymethods.conversion; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + */ +@Table +@Data +@NoArgsConstructor +class Contact { + + @Id String id; + + Address address; + List
    addresses; + + public Contact(String id) { + this.id = id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java new file mode 100644 index 000000000..2567ff361 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2016 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.test.integration.repository.querymethods.conversion; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.codehaus.jackson.map.ObjectMapper; +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.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base.PersonRepository; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StringUtils; + +/** + * Integration tests for query derivation through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + @EnableCassandraRepositories(considerNestedRepositories = true) + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Contact.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + + @Override + public CustomConversions customConversions() { + return new CustomConversions(Arrays.asList(AddressReadConverter.INSTANCE, AddressWriteConverter.INSTANCE)); + } + } + + @Autowired CassandraOperations template; + @Autowired ContactRepository contactRepository; + + Contact walter, flynn; + + @Before + public void before() { + + deleteAllEntities(); + + template.execute("CREATE INDEX IF NOT EXISTS contact_address ON contact (address);"); + template.execute("CREATE INDEX IF NOT EXISTS contact_addresses ON contact (addresses);"); + + walter = new Contact("Walter"); + walter.setAddress(new Address("Albuquerque", "USA")); + walter.setAddresses(Arrays.asList(new Address("Albuquerque", "USA"), new Address("New Hampshire", "USA"), + new Address("Grocery Store", "Mexico"))); + + flynn = new Contact("Flynn"); + flynn.setAddress(new Address("Albuquerque", "USA")); + flynn.setAddresses(Collections.singletonList(new Address("Albuquerque", "USA"))); + + walter = contactRepository.save(walter); + flynn = contactRepository.save(flynn); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByConvertedParameter() { + + List contacts = contactRepository.findByAddress(walter.getAddress()); + + assertThat(contacts, hasItems(walter, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByStringParameter() { + + String parameter = AddressWriteConverter.INSTANCE.convert(walter.getAddress()); + List contacts = contactRepository.findByAddress(parameter); + + assertThat(contacts, hasItems(walter, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void findByAddressesIn() { + + assertThat(contactRepository.findByAddressesContains(flynn.address), containsInAnyOrder(flynn, walter)); + assertThat(contactRepository.findByAddressesContains(walter.addresses.get(1)), contains(walter)); + } + + interface ContactRepository extends CassandraRepository { + + List findByAddress(Address address); + + List findByAddress(String address); + + List findByAddressesContains(Address address); + } + + /** + * @author Mark Paluch + */ + static enum AddressReadConverter implements Converter { + + INSTANCE; + + public Address convert(String source) { + + if (StringUtils.hasText(source)) { + try { + return new ObjectMapper().readValue(source, Address.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + return null; + } + } + + /** + * @author Mark Paluch + */ + static enum AddressWriteConverter implements Converter { + INSTANCE; + + public String convert(Address source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java index d8c38eae8..73fba9e21 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java @@ -20,17 +20,18 @@ import java.time.ZoneId; import java.util.Date; import org.springframework.cassandra.core.PrimaryKeyType; -import org.springframework.data.cassandra.mapping.Indexed; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; import lombok.Data; +import lombok.NoArgsConstructor; /** * Sample domain class. */ @Table @Data +@NoArgsConstructor public class Person { @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) private String lastname; @@ -44,4 +45,10 @@ public class Person { private LocalDate createdDate; private ZoneId zoneId; + + public Person(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java new file mode 100644 index 000000000..b7603fcc1 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 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.test.integration.repository.querymethods.derived; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; +import org.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +interface PersonRepository extends CassandraRepository { + + List findByLastname(String lastname); + + List findByLastname(String lastname, Sort sort); + + List findByLastnameOrderByFirstnameAsc(String lastname); + + Person findByFirstnameAndLastname(String firstname, String lastname); + + Person findByCreatedDate(LocalDate createdDate); + + Person findByNicknameStartsWith(String prefix); + + Person findByNicknameContains(String contains); + + Person findByNumberOfChildren(NumberOfChildren numberOfChildren); + + Collection findPersonProjectedBy(); + + @Query("select * from person where firstname = ?0 and lastname = 'White'") + List findByFirstname(String firstname); + + enum NumberOfChildren { + ZERO, ONE, TWO, + } + + interface PersonProjection { + + String getFirstname(); + + String getLastname(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java new file mode 100644 index 000000000..d7ad2bca6 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2016 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.test.integration.repository.querymethods.derived; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +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.Configuration; +import org.springframework.core.SpringVersion; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; +import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.NumberOfChildren; +import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.PersonProjection; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.CassandraVersion; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.data.domain.Sort; +import org.springframework.data.util.Version; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests for query derivation through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + @EnableCassandraRepositories + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Person.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + + } + + @Autowired CassandraOperations template; + @Autowired PersonRepository personRepository; + + Person walter, skyler, flynn; + + @Before + public void before() { + + deleteAllEntities(); + + Person person = new Person("Walter", "White"); + person.setNumberOfChildren(2); + + walter = personRepository.save(person); + skyler = personRepository.save(new Person("Skyler", "White")); + flynn = personRepository.save(new Person("Flynn (Walter Jr.)", "White")); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastname() { + + List result = personRepository.findByLastname("White"); + + assertThat(result, hasItems(walter, skyler, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastnameAndDynamicSort() { + + List result = personRepository.findByLastname("White", new Sort("firstname")); + + assertThat(result, contains(flynn, skyler, walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastnameWithOrdering() { + + List result = personRepository.findByLastnameOrderByFirstnameAsc("White"); + + assertThat(result, contains(flynn, skyler, walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByFirstnameAndLastname() { + + Person result = personRepository.findByFirstnameAndLastname("Walter", "White"); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void executesCollectionQueryWithProjectionCorrectly() { + + Collection collection = personRepository.findPersonProjectedBy(); + + assertThat(collection, hasSize(3)); + + for (PersonProjection personProjection : collection) { + assertThat(personProjection.getLastname(), is(equalTo("White"))); + } + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByNumberOfChildren() throws Exception { + + assumeThat(SpringVersion.getVersion(), startsWith("4.3")); + + template.execute("CREATE INDEX IF NOT EXISTS person_number_of_children ON person (numberofchildren);"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + Person result = personRepository.findByNumberOfChildren(NumberOfChildren.TWO); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLocalDate() throws InterruptedException { + + template.execute("CREATE INDEX IF NOT EXISTS person_created_date ON person (createddate);"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setCreatedDate(LocalDate.now()); + personRepository.save(walter); + + Person result = personRepository.findByCreatedDate(walter.getCreatedDate()); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseQueryOverride() { + + Person otherWalter = new Person("Walter", "Black"); + personRepository.save(otherWalter); + + List result = personRepository.findByFirstname("Walter"); + + assertThat(result, hasSize(1)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseStartsWithQuery() throws InterruptedException { + + Version version = CassandraVersion.get(template.getSession()); + assumeTrue(version.isGreaterThanOrEqualTo(Version.parse("3.4"))); + + template.execute( + "CREATE CUSTOM INDEX IF NOT EXISTS fn_starts_with ON person (nickname) USING 'org.apache.cassandra.index.sasi.SASIIndex';"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setNickname("Heisenberg"); + personRepository.save(walter); + + assertThat(personRepository.findByNicknameStartsWith("Heis"), is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseContainsQuery() throws InterruptedException { + + Version version = CassandraVersion.get(template.getSession()); + assumeTrue(version.isGreaterThanOrEqualTo(Version.parse("3.4"))); + + template.execute( + "CREATE CUSTOM INDEX IF NOT EXISTS fn_contains ON person (nickname) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n" + + "WITH OPTIONS = { 'mode': 'CONTAINS' };"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setNickname("Heisenberg"); + personRepository.save(walter); + + assertThat(personRepository.findByNicknameContains("eisenber"), is(walter)); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java index 0503034eb..927f1ae2b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java @@ -39,6 +39,11 @@ public abstract class AbstractSpringDataEmbeddedCassandraIntegrationTest */ public void deleteAllEntities() { for (CassandraPersistentEntity entity : template.getConverter().getMappingContext().getPersistentEntities()) { + + if(entity.getType().isInterface()){ + continue; + } + template.truncate(entity.getTableName()); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java new file mode 100644 index 000000000..1b9d73547 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 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.test.integration.support; + +import lombok.experimental.UtilityClass; + +import org.springframework.data.util.Version; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; + +/** + * Utility to retrieve the Cassandra release version. + * + * @author Mark Paluch + */ +@UtilityClass +public class CassandraVersion { + + /** + * Retrieve the Cassandra release version. + * + * @param session must not be {@literal null}. + * @return the release {@link Version}. + */ + public static Version get(Session session) { + + Assert.notNull(session, "Session must not be null"); + + ResultSet resultSet = session.execute("SELECT release_version FROM system.local;"); + Row row = resultSet.one(); + return Version.parse(row.getString(0)); + } +} diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml index ec5fbec6b..3a78b36da 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml @@ -1,34 +1,15 @@ - + entity-base-packages="org.springframework.data.cassandra.test.integration.repository.simple">