From 36ba47e5fcfe0042c3eb9bd983bef5144ed8d979 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 17 Mar 2017 15:07:41 +0100 Subject: [PATCH] DATACASS-343 - Introduce Query and Update objects. We now support fluent query creation for Cassandra queries via Query. Query predicates are built with Criteria and Query that also take QueryOptions, Sort and limiting. Query accepts a column specification to specify column inclusion/exclusion along with function specification (TTL, WRITETIME). Query query = Query.query(Criteria.where("userId").in("heisenberg", "mike")).and(Criteria.where("age").gt("51")); query = query.columns(Columns.from("userId", "age").include("firstname")) .with(new Sort("age")) .with(QueryOptions.builder().fetchSize(10).build()) .withAllowFiltering() .limit(10); List people = operations.select(query, Person.class); Query can be used with select, stream, update and delete Template API methods. We now support fluent update creation via Update to specify individual update actions for a selection of rows. Update supports the common Cassandra update assignments for singular, collection and map-typed columns and can increment/decrement counter columns. Update update = Update.update("lastname", "White") .addTo("firearms").appendAll("Ruger LCR", "M60") .remove("propertiesSet", "trust") .clear("friends"); Query query = Query.query(Criteria.where("id").is("heisenberg")); template.update(query, update, Person.class); Update can be used with the update Template API methods. --- .../cassandra/convert/CassandraConverter.java | 11 +- .../convert/MappingCassandraConverter.java | 19 + .../data/cassandra/convert/QueryMapper.java | 475 ++++++++++++ .../data/cassandra/convert/UpdateMapper.java | 245 ++++++ .../core/AsyncCassandraOperations.java | 59 ++ .../core/AsyncCassandraTemplate.java | 159 +++- .../cassandra/core/CassandraOperations.java | 60 ++ .../cassandra/core/CassandraTemplate.java | 75 +- .../core/ReactiveCassandraOperations.java | 46 ++ .../core/ReactiveCassandraTemplate.java | 184 +++-- .../data/cassandra/core/StatementFactory.java | 410 ++++++++++ .../data/cassandra/core/query/ColumnName.java | 198 +++++ .../data/cassandra/core/query/Columns.java | 464 ++++++++++++ .../data/cassandra/core/query/Criteria.java | 287 +++++++ .../core/query/CriteriaDefinition.java | 115 +++ .../cassandra/core/query/DefaultFilter.java | 54 ++ .../data/cassandra/core/query/Filter.java | 70 ++ .../data/cassandra/core/query/Query.java | 331 +++++++++ .../core/query/SerializationUtils.java | 141 ++++ .../data/cassandra/core/query/Update.java | 699 ++++++++++++++++++ .../query/AbstractCassandraQuery.java | 184 ++++- .../query/AbstractReactiveCassandraQuery.java | 12 +- .../query/CassandraQueryCreator.java | 154 +--- .../query/CassandraQueryExecution.java | 27 +- .../query/CassandraQueryMethod.java | 9 +- .../ExpressionEvaluatingParameterBinder.java | 1 + .../query/PartTreeCassandraQuery.java | 67 +- .../ReactiveCassandraParameterAccessor.java | 3 +- .../ReactiveCassandraQueryExecution.java | 19 +- .../query/ReactivePartTreeCassandraQuery.java | 67 +- .../ReactiveStringBasedCassandraQuery.java | 22 +- .../query/StringBasedCassandraQuery.java | 19 +- .../repository/query/StringBasedQuery.java | 160 +--- .../MappingCassandraEntityInformation.java | 3 +- .../cassandra/convert/CurrencyConverter.java | 36 + .../MappingCassandraConverterUnitTests.java | 6 +- .../convert/QueryMapperUnitTests.java | 321 ++++++++ .../convert/UpdateMapperUnitTests.java | 186 +++++ ...syncCassandraTemplateIntegrationTests.java | 88 +++ .../CassandraTemplateIntegrationTests.java | 128 +++- ...tiveCassandraTemplateIntegrationTests.java | 90 +++ .../core/StatementFactoryUnitTests.java | 244 ++++++ .../core/query/ColumnNameUnitTests.java | 78 ++ .../core/query/ColumnsUnitTests.java | 78 ++ .../core/query/CriteriaUnitTests.java | 128 ++++ .../cassandra/core/query/QueryUnitTests.java | 67 ++ .../cassandra/core/query/UpdateUnitTests.java | 137 ++++ .../data/cassandra/domain/CompositeKey.java | 2 +- .../query/CassandraQueryCreatorUnitTests.java | 15 +- .../PartTreeCassandraQueryUnitTests.java | 44 +- ...activePartTreeCassandraQueryUnitTests.java | 4 +- ...iveStringBasedCassandraQueryUnitTests.java | 25 +- .../StringBasedCassandraQueryUnitTests.java | 175 ++--- .../cassandra/support/UserTypeBuilder.java | 83 +++ 54 files changed, 6163 insertions(+), 621 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/QueryMapper.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/UpdateMapper.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/ColumnName.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Columns.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/CriteriaDefinition.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/DefaultFilter.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Filter.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Query.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/SerializationUtils.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Update.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CurrencyConverter.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/QueryMapperUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/UpdateMapperUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnNameUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnsUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/CriteriaUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/QueryUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/UpdateUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/UserTypeBuilder.java 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 7955a04b3..34dbd42db 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 @@ -70,7 +70,16 @@ public interface CassandraConverter * Converts the given object into one Cassandra will be able to store natively in a column. * * @param obj {@link Object} to convert, must not be {@literal null}. - * @param typeInformation {@link TypeInformation} used to describe the object type; must not be {@literal null}. + * @return the result of the conversion. + * @since 2.0 + */ + Optional convertToCassandraColumn(Optional obj); + + /** + * Converts the given object into one Cassandra will be able to store natively in a column. + * + * @param obj {@link Object} to convert, must not be {@literal null}. + * @param typeInformation {@link TypeInformation} used to describe the object type; may be {@literal null}. * @return the result of the conversion. * @since 1.5 */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java index 4205f5f52..b0cc0eaea 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java @@ -258,6 +258,19 @@ public class MappingCassandraConverter extends AbstractCassandraConverter throw new MappingException("Unknown row object " + ObjectUtils.nullSafeClassName(row)); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraConverter#convertToCassandraColumn(java.util.Optional) + */ + @Override + @SuppressWarnings("unchecked") + public Optional convertToCassandraColumn(Optional obj) { + + return convertToCassandraColumn(obj, + obj.map(Object::getClass) // + .map(ClassTypeInformation::from) // + .orElse((ClassTypeInformation) ClassTypeInformation.OBJECT)); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.convert.CassandraConverter#convertToCassandraColumn(java.util.Optional, org.springframework.data.util.TypeInformation) */ @@ -667,6 +680,12 @@ public class MappingCassandraConverter extends AbstractCassandraConverter }); } + if (getCustomConversions().isSimpleType(value.getClass())) { + // Doesn't need conversion + return getPotentiallyConvertedSimpleValue(optional, + typeInformation != null ? (Class) typeInformation.getType() : null); + } + TypeInformation type = (typeInformation != null ? typeInformation : ClassTypeInformation.from(value.getClass())); TypeInformation actualType = type.getActualType(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/QueryMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/QueryMapper.java new file mode 100644 index 000000000..7df2b7300 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/QueryMapper.java @@ -0,0 +1,475 @@ +/* + * Copyright 2017 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.convert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.ColumnName; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Columns.ColumnSelector; +import org.springframework.data.cassandra.core.query.Columns.FunctionCall; +import org.springframework.data.cassandra.core.query.Columns.Selector; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.CriteriaDefinition; +import org.springframework.data.cassandra.core.query.CriteriaDefinition.Predicate; +import org.springframework.data.cassandra.core.query.Filter; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PropertyReferenceException; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * Map {@link org.springframework.data.cassandra.core.query.Query} to CQL-specific data types. + * + * @author Mark Paluch + * @since 2.0 + */ +public class QueryMapper { + + private final CassandraConverter converter; + + private final MappingContext, CassandraPersistentProperty> mappingContext; + + /** + * Creates a new {@link QueryMapper} with the given {@link CassandraConverter}. + * + * @param converter must not be {@literal null}. + */ + public QueryMapper(CassandraConverter converter) { + + Assert.notNull(converter, "CassandraConverter must not be null"); + + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + } + + /** + * Map a {@link Filter} with a {@link CassandraPersistentEntity type hint}. Filter mapping translates property names + * to column names and maps {@link Predicate} values to simple Cassandra values. + * + * @param filter must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the mapped {@link Filter}. + */ + public Filter getMappedObject(Filter filter, CassandraPersistentEntity entity) { + + Assert.notNull(filter, "Filter must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + List result = new ArrayList<>(); + + for (CriteriaDefinition criteriaDefinition : filter) { + + Field field = createPropertyField(entity, criteriaDefinition.getColumnName()); + + Predicate predicate = criteriaDefinition.getPredicate(); + + Optional value = Optional.ofNullable(predicate.getValue()); + TypeInformation typeInformation = getTypeInformation(field, value); + Optional mappedValue = converter.convertToCassandraColumn(value, typeInformation); + + Predicate mappedPredicate = new Predicate(predicate.getOperator(), mappedValue.orElse(null)); + result.add(Criteria.of(field.getMappedKey(), mappedPredicate)); + } + + return Filter.from(result); + } + + /** + * Return {@link ColumnSelector}s for all columns of {@link CassandraPersistentEntity}. + * + * @param entity must not be {@literal null}. + * @return {@link ColumnSelector}s for all columns of {@link CassandraPersistentEntity}. + */ + public List getColumns(CassandraPersistentEntity entity) { + + return entity.getPersistentProperties() // + .flatMap(p -> p.getColumnNames().stream()).map(ColumnSelector::from) // + .collect(Collectors.toList()); + } + + /** + * Map {@link Columns} with a {@link CassandraPersistentEntity type hint} to {@link ColumnSelector}s. + * + * @param columns must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the mapped {@link Selector}s. + */ + public List getMappedSelectors(Columns columns, CassandraPersistentEntity entity) { + + Assert.notNull(columns, "Columns must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + if (columns.isEmpty()) { + return Collections.emptyList(); + } + + List selectors = new ArrayList<>(); + + for (ColumnName column : columns) { + + Field field = createPropertyField(entity, column); + + columns.getSelector(column).ifPresent(selector -> { + getCqlIdentifier(column, field).ifPresent(cqlIdentifier -> { + selectors.add(getMappedSelector(selector, cqlIdentifier)); + }); + }); + } + + if (columns.isEmpty()) { + + entity.doWithProperties((PropertyHandler) property -> { + + if (property.isCompositePrimaryKey()) { + for (CqlIdentifier cqlIdentifier : property.getColumnNames()) { + selectors.add(ColumnSelector.from(cqlIdentifier.toCql())); + } + } else { + selectors.add(ColumnSelector.from(property.getColumnName().toCql())); + } + }); + } + + return selectors; + } + + private Selector getMappedSelector(Selector selector, CqlIdentifier cqlIdentifier) { + + if (selector instanceof ColumnSelector) { + + ColumnSelector columnSelector = (ColumnSelector) selector; + + ColumnSelector mappedColumnSelector = ColumnSelector.from(cqlIdentifier); + + return columnSelector.getAlias() // + .map(mappedColumnSelector::as) // + .orElse(mappedColumnSelector); + } + + if (selector instanceof FunctionCall) { + + FunctionCall functionCall = (FunctionCall) selector; + + List mappedParameters = functionCall.getParameters() // + .stream() // + .map(o -> { + + if (o instanceof Selector) { + return getMappedSelector((Selector) o, cqlIdentifier); + } + + return o; + }) // + .collect(Collectors.toList()); + + FunctionCall mappedCall = FunctionCall.from(functionCall.getExpression(), mappedParameters.toArray()); + + return functionCall.getAlias() // + .map(mappedCall::as) // + .orElse(mappedCall); + } + + throw new IllegalArgumentException(String.format("Selector [%s] not supported", selector)); + } + + /** + * Map {@link Columns} with a {@link CassandraPersistentEntity type hint} to column names for included columns. + * Function call selectors or other {@link org.springframework.data.cassandra.core.query.Columns.Selector} types are + * not included. + * + * @param columns must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the mapped column names. + */ + public List getMappedColumnNames(Columns columns, CassandraPersistentEntity entity) { + + Assert.notNull(columns, "Columns must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + if (columns.isEmpty()) { + return Collections.emptyList(); + } + + List columnNames = new ArrayList<>(); + + Set> seen = new HashSet<>(); + + for (ColumnName column : columns) { + + Field field = createPropertyField(entity, column); + + field.getProperty().ifPresent(seen::add); + + columns.getSelector(column) // + .filter(selector -> selector instanceof ColumnSelector) // + .ifPresent(columnExpression -> { + + getCqlIdentifier(column, field) // + .map(CqlIdentifier::toCql) // + .ifPresent(columnNames::add); + }); + } + + if (columns.isEmpty()) { + + entity.doWithProperties((PropertyHandler) property -> { + + if (property.isCompositePrimaryKey()) { + return; + } + + if (seen.add(property)) { + columnNames.add(property.getColumnName().toCql()); + } + }); + } + + return columnNames; + } + + public Sort getMappedSort(Sort sort, CassandraPersistentEntity entity) { + + Assert.notNull(sort, "Sort must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + if (!sort.iterator().hasNext()) { + return sort; + } + + List mappedOrders = new ArrayList<>(); + + for (Order order : sort) { + + ColumnName columnName = ColumnName.from(order.getProperty()); + Field field = createPropertyField(entity, columnName); + + Order mappedOrder = getCqlIdentifier(columnName, field) + .map(cqlIdentifier -> new Order(order.getDirection(), cqlIdentifier.toCql())).orElse(order); + mappedOrders.add(mappedOrder); + } + + return new Sort(mappedOrders); + } + + private Optional getCqlIdentifier(ColumnName column, Field field) { + + try { + + if (field.getProperty().isPresent()) { + return field.getProperty().map(CassandraPersistentProperty::getColumnName); + } + + if (column.getColumnName().isPresent()) { + return column.getColumnName().map(CqlIdentifier::cqlId); + } + + return column.getCqlIdentifier(); + + } catch (IllegalStateException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + /** + * @param entity + * @param key + * @return + */ + protected Field createPropertyField(CassandraPersistentEntity entity, ColumnName key) { + return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext); + } + + @SuppressWarnings("unchecked") + TypeInformation getTypeInformation(Field field, Optional value) { + + return field.getProperty().map(CassandraPersistentProperty::getTypeInformation).orElseGet(() -> { + + return value.map(Object::getClass) // + .map(ClassTypeInformation::from) // + .orElse((ClassTypeInformation) ClassTypeInformation.OBJECT); + + }); + } + + /** + * Value object to represent a field and its meta-information. + * + * @author Mark Paluch + */ + protected static class Field { + + protected final ColumnName name; + + /** + * Creates a new {@link Field} without meta-information but the given name. + * + * @param name must not be {@literal null} or empty. + */ + public Field(ColumnName name) { + + Assert.notNull(name, "Name must not be null!"); + this.name = name; + } + + /** + * Returns a new {@link Field} with the given name. + * + * @param name must not be {@literal null} or empty. + * @return + */ + public Field with(ColumnName name) { + return new Field(name); + } + + /** + * Returns the underlying {@link CassandraPersistentProperty} backing the field. For path traversals this will be + * the property that represents the value to handle. This means it'll be the leaf property for plain paths or the + * association property in case we refer to an association somewhere in the path. + * + * @return + */ + public Optional getProperty() { + return Optional.empty(); + } + + /** + * Returns the key to be used in the mapped document eventually. + * + * @return + */ + public ColumnName getMappedKey() { + return name; + } + } + + /** + * Extension of {@link Field} to be backed with mapping metadata. + * + * @author Mark Paluch + */ + protected static class MetadataBackedField extends Field { + + private final CassandraPersistentEntity entity; + private final MappingContext, CassandraPersistentProperty> mappingContext; + private final Optional> path; + private final CassandraPersistentProperty property; + private final Optional optionalProperty; + + /** + * Creates a new {@link MetadataBackedField} with the given name, {@link MongoPersistentEntity} and + * {@link MappingContext}. + * + * @param name must not be {@literal null} or empty. + * @param entity must not be {@literal null}. + * @param context must not be {@literal null}. + */ + public MetadataBackedField(ColumnName name, CassandraPersistentEntity entity, + MappingContext, CassandraPersistentProperty> context) { + this(name, entity, context, null); + } + + /** + * Creates a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentProperty} and + * {@link MappingContext} with the given {@link CassandraPersistentProperty}. + * + * @param name must not be {@literal null} or empty. + * @param entity must not be {@literal null}. + * @param context must not be {@literal null}. + * @param property may be {@literal null}. + */ + public MetadataBackedField(ColumnName name, CassandraPersistentEntity entity, + MappingContext, CassandraPersistentProperty> context, + CassandraPersistentProperty property) { + + super(name); + + Assert.notNull(entity, "MongoPersistentEntity must not be null!"); + + this.entity = entity; + this.mappingContext = context; + this.path = getPath(name.toCql()); + this.property = path.map(PersistentPropertyPath::getLeafProperty).orElse(property); + this.optionalProperty = Optional.ofNullable(this.property); + } + + /** + * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}. + * + * @param pathExpression + * @return + */ + private Optional> getPath(String pathExpression) { + + try { + PropertyPath path = PropertyPath.from(pathExpression.replaceAll("\\.\\d", ""), entity.getTypeInformation()); + PersistentPropertyPath propertyPath = mappingContext + .getPersistentPropertyPath(path); + + return Optional.of(propertyPath); + } catch (PropertyReferenceException e) { + return Optional.empty(); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#with(java.lang.String) + */ + @Override + public MetadataBackedField with(ColumnName name) { + return new MetadataBackedField(name, entity, mappingContext, property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getProperty() + */ + @Override + public Optional getProperty() { + return optionalProperty; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getTargetKey() + */ + @Override + public ColumnName getMappedKey() { + + return path.map(PersistentPropertyPath::getLeafProperty) // + .map(CassandraPersistentProperty::getColumnName) // + .map(ColumnName::from) // + .orElse(name); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/UpdateMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/UpdateMapper.java new file mode 100644 index 000000000..adf1ea554 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/UpdateMapper.java @@ -0,0 +1,245 @@ +/* + * Copyright 2017 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.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.springframework.data.cassandra.core.query.Filter; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.data.cassandra.core.query.Update.AddToMapOp; +import org.springframework.data.cassandra.core.query.Update.AddToOp; +import org.springframework.data.cassandra.core.query.Update.AssignmentOp; +import org.springframework.data.cassandra.core.query.Update.IncrOp; +import org.springframework.data.cassandra.core.query.Update.RemoveOp; +import org.springframework.data.cassandra.core.query.Update.SetAtIndexOp; +import org.springframework.data.cassandra.core.query.Update.SetAtKeyOp; +import org.springframework.data.cassandra.core.query.Update.SetOp; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.Name; + +/** + * Map {@link org.springframework.data.cassandra.core.query.Update} to CQL-specific data types. + * + * @author Mark Paluch + */ +public class UpdateMapper extends QueryMapper { + + private final CassandraConverter converter; + + private final CassandraMappingContext mappingContext; + + /** + * Creates a new {@link UpdateMapper} with the given {@link CassandraConverter}. + * + * @param converter must not be {@literal null}. + */ + public UpdateMapper(CassandraConverter converter) { + + super(converter); + + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + } + + /** + * Map a {@link Update} with a {@link CassandraPersistentEntity type hint}. Update mapping translates property names + * to column names and maps {@link AssignmentOp update operation} values to simple Cassandra values. + * + * @param update must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the mapped {@link Filter}. + */ + public Update getMappedObject(Update update, CassandraPersistentEntity entity) { + + Assert.notNull(update, "Update must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + Collection assignmentOperations = update.getUpdateOperations(); + List mapped = new ArrayList<>(assignmentOperations.size()); + + for (AssignmentOp assignmentOp : assignmentOperations) { + + Field field = createPropertyField(entity, assignmentOp.getColumnName()); + + mapped.add(getMappedUpdateOperation(assignmentOp, field)); + } + + return Update.of(mapped); + } + + private AssignmentOp getMappedUpdateOperation(AssignmentOp assignmentOp, Field field) { + + if (assignmentOp instanceof SetOp) { + return getMappedUpdateOperation(field, (SetOp) assignmentOp); + } + + if (assignmentOp instanceof RemoveOp) { + return getMappedUpdateOperation(field, (RemoveOp) assignmentOp); + } + + if (assignmentOp instanceof IncrOp) { + return new IncrOp(field.getMappedKey(), ((IncrOp) assignmentOp).getValue()); + } + + if (assignmentOp instanceof AddToOp) { + return getMappedUpdateOperation(field, (AddToOp) assignmentOp); + } + + if (assignmentOp instanceof AddToMapOp) { + return getMappedUpdateOperation(field, (AddToMapOp) assignmentOp); + } + + throw new IllegalArgumentException(String.format("UpdateOp %s not supported", assignmentOp)); + } + + private AssignmentOp getMappedUpdateOperation(Field field, SetOp updateOp) { + + Optional value = Optional.ofNullable(updateOp.getValue()); + + if (updateOp instanceof SetAtKeyOp) { + + SetAtKeyOp op = (SetAtKeyOp) updateOp; + + Optional> typeInformation = field.getProperty() + .map(PersistentProperty::getTypeInformation); + Optional> keyType = typeInformation.map(TypeInformation::getActualType); + Optional> valueType = typeInformation.flatMap(TypeInformation::getMapValueType); + + Optional k = Optional.ofNullable(op.getKey()); + Optional v = Optional.ofNullable(op.getValue()); + + Optional mappedKey = keyType.map(it -> converter.convertToCassandraColumn(k, it)) + .orElseGet(() -> converter.convertToCassandraColumn(k)); + + Optional mappedValue = valueType.map(it -> converter.convertToCassandraColumn(v, it)) + .orElseGet(() -> converter.convertToCassandraColumn(v)); + + return new SetAtKeyOp(field.getMappedKey(), mappedKey.orElse(null), mappedValue.orElse(null)); + } + + TypeInformation typeInformation = getTypeInformation(field, value); + + if (updateOp instanceof SetAtIndexOp) { + + SetAtIndexOp op = (SetAtIndexOp) updateOp; + + Optional mappedValue = converter.convertToCassandraColumn(Optional.ofNullable(op.getValue()), + typeInformation); + return new SetAtIndexOp(field.getMappedKey(), op.getIndex(), mappedValue.orElse(null)); + } + + if (updateOp.getValue() instanceof Collection && typeInformation.isCollectionLike()) { + + Collection collection = (Collection) updateOp.getValue(); + + if (collection.isEmpty()) { + + DataType.Name dataType = field.getProperty() // + .map(mappingContext::getDataType) // + .map(DataType::getName) // + .orElse(Name.LIST); + + if (dataType == Name.SET) { + return new SetOp(field.getMappedKey(), Collections.emptySet()); + } + + return new SetOp(field.getMappedKey(), Collections.emptyList()); + } + } + + Optional mappedValue = converter.convertToCassandraColumn(value, typeInformation); + return new SetOp(field.getMappedKey(), mappedValue.orElse(null)); + } + + private AssignmentOp getMappedUpdateOperation(Field field, RemoveOp updateOp) { + + Optional value = Optional.ofNullable(updateOp.getValue()); + TypeInformation typeInformation = getTypeInformation(field, value); + + Optional mappedValue = converter.convertToCassandraColumn(value, typeInformation); + return new RemoveOp(field.getMappedKey(), mappedValue.orElse(null)); + } + + @SuppressWarnings("unchecked") + private AssignmentOp getMappedUpdateOperation(Field field, AddToOp updateOp) { + + Optional> value = Optional.ofNullable(updateOp.getValue()); + TypeInformation typeInformation = getTypeInformation(field, value); + + Collection mappedValue = (Collection) converter.convertToCassandraColumn(value, typeInformation) + .orElse(null); + + if (field.getProperty().isPresent()) { + + DataType dataType = mappingContext.getDataType(field.getProperty().get()); + if (dataType.getName() == Name.SET && !(mappedValue instanceof Set)) { + + Collection collection = new HashSet<>(); + collection.addAll(mappedValue); + mappedValue = collection; + } + + if (dataType.getName() == Name.LIST && !(mappedValue instanceof List)) { + + Collection collection = new ArrayList<>(); + collection.addAll(mappedValue); + mappedValue = collection; + } + } + + return new AddToOp(field.getMappedKey(), mappedValue, updateOp.getMode()); + } + + private AssignmentOp getMappedUpdateOperation(Field field, AddToMapOp updateOp) { + + Optional> typeInformation = field.getProperty() + .map(PersistentProperty::getTypeInformation); + Optional> keyType = typeInformation.map(TypeInformation::getActualType); + Optional> valueType = typeInformation.flatMap(TypeInformation::getMapValueType); + + Map result = new LinkedHashMap<>(updateOp.getValue().size(), 1); + + updateOp.getValue().forEach((k, v) -> { + + Optional key = Optional.ofNullable(k); + Optional value = Optional.ofNullable(v); + + Optional mappedKey = keyType.map(it -> converter.convertToCassandraColumn(key, it)) + .orElseGet(() -> converter.convertToCassandraColumn(key)); + + Optional mappedValue = valueType.map(it -> converter.convertToCassandraColumn(value, it)) + .orElseGet(() -> converter.convertToCassandraColumn(value)); + + result.put(mappedKey.orElse(null), mappedValue.orElse(null)); + }); + + return new AddToMapOp(field.getMappedKey(), result); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java index 7ffd61de4..dae6762ae 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java @@ -23,6 +23,8 @@ import org.springframework.cassandra.core.QueryOptions; import org.springframework.cassandra.core.WriteOptions; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import org.springframework.util.concurrent.ListenableFuture; import com.datastax.driver.core.Statement; @@ -128,6 +130,63 @@ public interface AsyncCassandraOperations { */ ListenableFuture selectOne(Statement statement, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link List} of entities. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the converted results + * @throws DataAccessException if there is any problem executing the query. + */ + ListenableFuture> select(Query query, Class entityClass) throws DataAccessException; + + /** + * Execute a {@code SELECT} query and convert the resulting items notifying {@link Consumer} for each entity. + * + * @param query must not be {@literal null}. + * @param entityConsumer object that will be notified on each entity, one object at a time, must not be + * {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the completion handle + * @throws DataAccessException if there is any problem executing the query. + */ + ListenableFuture select(Query query, Consumer entityConsumer, Class entityClass) + throws DataAccessException; + + /** + * Execute a {@code SELECT} query and convert the resulting item to an entity. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the converted object or {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + */ + ListenableFuture selectOne(Query query, Class entityClass) throws DataAccessException; + + /** + * Update the queried entities and return {@literal true} if the update was applied. + * + * @param query must not be {@literal null}. + * @param update must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + */ + ListenableFuture update(Query query, Update update, Class entityClass) throws DataAccessException; + + /** + * Remove entities (rows)/columns from the table by {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return {@literal true} if the deletion was applied. + * @throws DataAccessException if there is any problem executing the query. + */ + ListenableFuture delete(Query query, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java index 1dae1097d..5929d557e 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java @@ -34,6 +34,9 @@ import org.springframework.cassandra.core.support.CQLExceptionTranslator; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.convert.QueryMapper; +import org.springframework.data.cassandra.convert.UpdateMapper; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.util.Assert; @@ -78,6 +81,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { private final CQLExceptionTranslator exceptionTranslator; + private final StatementFactory statementFactory; + /** * Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link Session} and a default * {@link MappingCassandraConverter}. @@ -137,6 +142,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { this.mappingContext = converter.getMappingContext(); this.cqlOperations = asyncCqlTemplate; this.exceptionTranslator = asyncCqlTemplate.getExceptionTranslator(); + this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /* @@ -145,7 +151,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { */ @Override public AsyncCqlOperations getAsyncCqlOperations() { - return cqlOperations; + return this.cqlOperations; } /* @@ -154,9 +160,10 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { */ @Override public CassandraConverter getConverter() { - return converter; + return this.converter; } + /* (non-Javadoc) */ private static MappingCassandraConverter newConverter() { MappingCassandraConverter converter = new MappingCassandraConverter(); @@ -166,6 +173,32 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { return converter; } + /** + * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data used to + * store (map) objects to Cassandra tables. + * + * @return the {@link CassandraMappingContext} used by this template. + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext + */ + protected CassandraMappingContext getMappingContext() { + return this.mappingContext; + } + + /** + * Returns the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. + * + * @return the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. + * @see org.springframework.data.cassandra.core.StatementFactory + */ + protected StatementFactory getStatementFactory() { + return this.statementFactory; + } + + /* (non-Javadoc) */ + private CqlIdentifier getTableName(Object entity) { + return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); + } + // ------------------------------------------------------------------------- // Methods dealing with static CQL // ------------------------------------------------------------------------- @@ -220,7 +253,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return cqlOperations.query(statement, (row, rowNum) -> converter.read(entityClass, row)); + return getAsyncCqlOperations().query(statement, (row, rowNum) -> getConverter().read(entityClass, row)); } @Override @@ -231,8 +264,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entityConsumer, "Entity Consumer must not be empty"); Assert.notNull(entityClass, "Entity type must not be null"); - return cqlOperations.query(statement, (row) -> { - entityConsumer.accept(converter.read(entityClass, row)); + return getAsyncCqlOperations().query(statement, row -> { + entityConsumer.accept(getConverter().read(entityClass, row)); }); } @@ -247,6 +280,79 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { list -> list.stream().findFirst().orElse(null)); } + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public ListenableFuture> select(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return select(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), + entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.util.function.Consumer, java.lang.Class) + */ + @Override + public ListenableFuture select(Query query, Consumer entityConsumer, Class entityClass) + throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityConsumer, "Entity Consumer must not be empty"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return select(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), + entityConsumer, entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public ListenableFuture selectOne(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return selectOne(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), + entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class) + */ + @Override + public ListenableFuture update(Query query, org.springframework.data.cassandra.core.query.Update update, + Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(update, "Update must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return getAsyncCqlOperations().execute(getStatementFactory().update(query, update, + getMappingContext().getRequiredPersistentEntity(entityClass))); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public ListenableFuture delete(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return getAsyncCqlOperations().execute(getStatementFactory().delete(query, + getMappingContext().getRequiredPersistentEntity(entityClass))); + } + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- @@ -261,9 +367,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); Select select = QueryBuilder.select().countAll() - .from(mappingContext.getRequiredPersistentEntity(entityClass).getTableName().toCql()); + .from(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); - return cqlOperations.queryForObject(select, Long.class); + return getAsyncCqlOperations().queryForObject(select, Long.class); } /* @@ -276,13 +382,13 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().from(entity.getTableName().toCql()); - converter.write(id, select.where(), entity); + getConverter().write(id, select.where(), entity); - return new MappingListenableFutureAdapter<>(cqlOperations.queryForResultSet(select), + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().queryForResultSet(select), resultSet -> resultSet.iterator().hasNext()); } @@ -296,11 +402,11 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().all().from(entity.getTableName().toCql()); - converter.write(id, select.where(), entity); + getConverter().write(id, select.where(), entity); return selectOne(select, entityClass); } @@ -323,9 +429,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entity, "Entity must not be null"); - Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, converter); + Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, getConverter()); - return new MappingListenableFutureAdapter<>(cqlOperations.execute(new AsyncStatementCallback(insert)), + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(insert)), resultSet -> resultSet.wasApplied() ? entity : null); } @@ -347,9 +453,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entity, "Entity must not be null"); - Update update = QueryUtils.createUpdateQuery(getTableName(entity).toCql(), entity, options, converter); + Update update = QueryUtils.createUpdateQuery(getTableName(entity).toCql(), entity, options, getConverter()); - return new MappingListenableFutureAdapter<>(cqlOperations.execute(new AsyncStatementCallback(update)), + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(update)), resultSet -> resultSet.wasApplied() ? entity : null); } @@ -371,9 +477,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entity, "Entity must not be null"); - Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, converter); + Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, getConverter()); - return new MappingListenableFutureAdapter<>(cqlOperations.execute(new AsyncStatementCallback(delete)), + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(delete)), resultSet -> resultSet.wasApplied() ? entity : null); } @@ -387,13 +493,13 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Delete delete = QueryBuilder.delete().from(entity.getTableName().toCql()); - converter.write(id, delete.where(), entity); + getConverter().write(id, delete.where(), entity); - return cqlOperations.execute(delete); + return getAsyncCqlOperations().execute(delete); } /* @@ -405,15 +511,10 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); - Truncate truncate = QueryBuilder - .truncate(mappingContext.getRequiredPersistentEntity(entityClass).getTableName().toCql()); + Truncate truncate = QueryBuilder.truncate( + getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); - return new MappingListenableFutureAdapter<>(cqlOperations.execute(truncate), aBoolean -> null); - } - - private CqlIdentifier getTableName(Object entity) { - - return mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(truncate), aBoolean -> null); } private static class MappingListenableFutureAdapter diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java index 844e51b2d..720bf34e9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java @@ -25,6 +25,8 @@ import org.springframework.cassandra.core.WriteOptions; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import com.datastax.driver.core.Statement; @@ -149,6 +151,64 @@ public interface CassandraOperations { */ T selectOne(Statement statement, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link List} of entities. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the converted results + * @throws DataAccessException if there is any problem executing the query. + * @since 2.0 + */ + List select(Query query, Class entityClass) throws DataAccessException; + + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link Iterator} of entities. + *

+ * Returns a {@link Iterator} that wraps the Cassandra {@link com.datastax.driver.core.ResultSet}. + * + * @param element return type. + * @param query query to execute. Must not be empty or {@literal null}. + * @param entityClass Class type of the elements in the {@link Iterator} stream. Must not be {@literal null}. + * @return an {@link Iterator} (stream) over the elements in the query result set. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.0 + */ + Stream stream(Query query, Class entityClass) throws DataAccessException; + + /** + * Execute a {@code SELECT} query and convert the resulting item to an entity. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the converted object or {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + * @since 2.0 + */ + T selectOne(Query query, Class entityClass) throws DataAccessException; + + /** + * Update the queried entities and return {@literal true} if the update was applied. + * + * @param query must not be {@literal null}. + * @param update must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + */ + boolean update(Query query, Update update, Class entityClass) throws DataAccessException; + + /** + * Remove entities (rows)/columns from the table by {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + */ + boolean delete(Query query, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index ebc0ea1ac..b3a3addf6 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -34,6 +34,9 @@ import org.springframework.cassandra.core.session.SessionFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.convert.QueryMapper; +import org.springframework.data.cassandra.convert.UpdateMapper; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; @@ -76,6 +79,8 @@ public class CassandraTemplate implements CassandraOperations { private final CqlOperations cqlOperations; + private final StatementFactory statementFactory; + /** * Creates an instance of {@link CassandraTemplate} initialized with the given {@link Session} and a default * {@link MappingCassandraConverter}. @@ -132,8 +137,9 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(converter, "CassandraConverter must not be null"); this.converter = converter; - this.mappingContext = converter.getMappingContext(); this.cqlOperations = cqlOperations; + this.mappingContext = converter.getMappingContext(); + this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } private static MappingCassandraConverter newConverter() { @@ -228,6 +234,73 @@ public class CassandraTemplate implements CassandraOperations { return result.stream().findFirst().orElse(null); } + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public List select(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return select(statementFactory.select(query, mappingContext.getRequiredPersistentEntity(entityClass)), entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#stream(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Stream stream(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return stream(statementFactory.select(query, mappingContext.getRequiredPersistentEntity(entityClass)), entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public T selectOne(Query query, Class entityClass) throws DataAccessException { + + List result = select(query, entityClass); + + return (result.isEmpty() ? null : result.get(0)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class) + */ + @Override + public boolean update(Query query, org.springframework.data.cassandra.core.query.Update update, Class entityClass) + throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(update, "Update must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return cqlOperations + .execute(statementFactory.update(query, update, mappingContext.getRequiredPersistentEntity(entityClass))); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public boolean delete(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return cqlOperations + .execute(statementFactory.delete(query, mappingContext.getRequiredPersistentEntity(entityClass))); + } + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java index cdae7afe9..30a4da53a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraOperations.java @@ -24,6 +24,8 @@ import org.springframework.cassandra.core.ReactiveCqlOperations; import org.springframework.cassandra.core.WriteOptions; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import com.datastax.driver.core.Statement; @@ -87,6 +89,50 @@ public interface ReactiveCassandraOperations { */ Mono selectOne(Statement statement, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + + /** + * Execute a {@code SELECT} query and convert the resulting items to a stream of entities. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the result objects returned by the action. + * @throws DataAccessException if there is any problem issuing the execution. + */ + Flux select(Query query, Class entityClass) throws DataAccessException; + + /** + * Execute a {@code SELECT} query and convert the resulting item to an entity. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return the result object returned by the action or {@link Mono#empty()} + * @throws DataAccessException if there is any problem issuing the execution. + */ + Mono selectOne(Query query, Class entityClass) throws DataAccessException; + + /** + * Update the queried entities and return {@literal true} if the update was applied. + * + * @param query must not be {@literal null}. + * @param update must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @throws DataAccessException if there is any problem executing the query. + */ + Mono update(Query query, Update update, Class entityClass) throws DataAccessException; + + /** + * Remove entities (rows)/columns from the table by {@link Query}. + * + * @param query must not be {@literal null}. + * @param entityClass The entity type must not be {@literal null}. + * @return {@literal true} if the deletion was applied. + * @throws DataAccessException if there is any problem issuing the execution. + */ + Mono delete(Query query, Class entityClass) throws DataAccessException; + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java index 5a502bb99..6d883da95 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java @@ -18,7 +18,6 @@ package org.springframework.data.cassandra.core; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.reactivestreams.Publisher; import org.springframework.cassandra.core.CqlProvider; import org.springframework.cassandra.core.QueryOptions; import org.springframework.cassandra.core.ReactiveCqlOperations; @@ -33,11 +32,16 @@ import org.springframework.cassandra.core.session.ReactiveSessionFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.convert.QueryMapper; +import org.springframework.data.cassandra.convert.UpdateMapper; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.reactivestreams.Publisher; + import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; @@ -72,6 +76,8 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { private final ReactiveCqlOperations cqlOperations; + private final StatementFactory statementFactory; + /** * Creates an instance of {@link ReactiveCassandraTemplate} initialized with the given {@link ReactiveSession} and a * default {@link MappingCassandraConverter}. @@ -114,8 +120,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(converter, "CassandraConverter must not be null"); this.converter = converter; - this.mappingContext = this.converter.getMappingContext(); this.cqlOperations = new ReactiveCqlTemplate(sessionFactory); + this.mappingContext = this.converter.getMappingContext(); + this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /** @@ -135,10 +142,20 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(converter, "CassandraConverter must not be null"); this.converter = converter; - this.mappingContext = this.converter.getMappingContext(); this.cqlOperations = reactiveCqlOperations; + this.mappingContext = this.converter.getMappingContext(); + this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#getConverter() + */ + @Override + public CassandraConverter getConverter() { + return this.converter; + } + + /* (non-Javadoc) */ private static MappingCassandraConverter newConverter() { MappingCassandraConverter converter = new MappingCassandraConverter(); @@ -148,6 +165,41 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { return converter; } + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getReactiveCqlOperations() + */ + @Override + public ReactiveCqlOperations getReactiveCqlOperations() { + return cqlOperations; + } + + /** + * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data used to + * store (map) objects to Cassandra tables. + * + * @return the {@link CassandraMappingContext} used by this template. + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext + */ + protected CassandraMappingContext getMappingContext() { + return this.mappingContext; + } + + /** + * Returns the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. + * + * @return the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. + * @see org.springframework.data.cassandra.core.StatementFactory + */ + protected StatementFactory getStatementFactory() { + return this.statementFactory; + } + + /* (non-Javadoc) */ + private CqlIdentifier getTableName(Object entity) { + return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); + } + // ------------------------------------------------------------------------- // Methods dealing with static CQL // ------------------------------------------------------------------------- @@ -187,7 +239,7 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(cql, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return cqlOperations.query(cql, (row, rowNum) -> converter.read(entityClass, row)); + return getReactiveCqlOperations().query(cql, (row, rowNum) -> getConverter().read(entityClass, row)); } /* @@ -199,6 +251,64 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { return select(statement, entityClass).next(); } + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.cassandra.core.query.Query + // ------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Flux select(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return select(getStatementFactory().select(query, + getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Mono selectOne(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return selectOne(getStatementFactory().select(query, + getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class) + */ + @Override + public Mono update(Query query, org.springframework.data.cassandra.core.query.Update update, + Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(update, "Update must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return getReactiveCqlOperations().execute(getStatementFactory().update(query, update, + getMappingContext().getRequiredPersistentEntity(entityClass))); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) + */ + @Override + public Mono delete(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + return getReactiveCqlOperations().execute(getStatementFactory().delete(query, + getMappingContext().getRequiredPersistentEntity(entityClass))); + } + // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- @@ -213,11 +323,11 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().all().from(entity.getTableName().toCql()); - converter.write(id, select.where(), entity); + getConverter().write(id, select.where(), entity); return selectOne(select, entityClass); } @@ -232,13 +342,13 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().from(entity.getTableName().toCql()); - converter.write(id, select.where(), entity); + getConverter().write(id, select.where(), entity); - return cqlOperations.queryForRows(select).hasElements(); + return getReactiveCqlOperations().queryForRows(select).hasElements(); } /* @@ -251,9 +361,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); Select select = QueryBuilder.select().countAll() - .from(mappingContext.getRequiredPersistentEntity(entityClass).getTableName().toCql()); + .from(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); - return cqlOperations.queryForObject(select, Long.class); + return getReactiveCqlOperations().queryForObject(select, Long.class); } /* @@ -274,14 +384,14 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(entity, "Entity must not be null"); - Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, converter); + Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, getConverter()); class InsertCallback implements ReactiveSessionCallback, CqlProvider { @Override public Publisher doInSession(ReactiveSession session) throws DriverException, DataAccessException { - return session.execute(insert) - .flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); + return session.execute(insert).flatMap( + reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); } @Override @@ -290,7 +400,7 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { } } - return cqlOperations.execute(new InsertCallback()).next(); + return getReactiveCqlOperations().execute(new InsertCallback()).next(); } /* @@ -348,7 +458,7 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { } } - return cqlOperations.execute(new UpdateCallback()).next(); + return getReactiveCqlOperations().execute(new UpdateCallback()).next(); } /* @@ -382,13 +492,13 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + CassandraPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(entityClass); Delete delete = QueryBuilder.delete().from(entity.getTableName().toCql()); - converter.write(id, delete.where(), entity); + getConverter().write(id, delete.where(), entity); - return cqlOperations.execute(delete); + return getReactiveCqlOperations().execute(delete); } /* @@ -409,14 +519,14 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(entity, "Entity must not be null"); - Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, converter); + Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, getConverter()); class DeleteCallback implements ReactiveSessionCallback, CqlProvider { @Override public Publisher doInSession(ReactiveSession session) throws DriverException, DataAccessException { - return session.execute(delete) - .flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); + return session.execute(delete).flatMap( + reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); } @Override @@ -425,7 +535,7 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { } } - return cqlOperations.execute(new DeleteCallback()).next(); + return getReactiveCqlOperations().execute(new DeleteCallback()).next(); } /* @@ -458,31 +568,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); - Truncate truncate = QueryBuilder - .truncate(mappingContext.getRequiredPersistentEntity(entityClass).getTableName().toCql()); + Truncate truncate = QueryBuilder.truncate( + getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); - return cqlOperations.execute(truncate).then(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getConverter() - */ - @Override - public CassandraConverter getConverter() { - return converter; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getReactiveCqlOperations() - */ - @Override - public ReactiveCqlOperations getReactiveCqlOperations() { - return cqlOperations; - } - - private CqlIdentifier getTableName(Object entity) { - return mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); + return getReactiveCqlOperations().execute(truncate).then(); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java new file mode 100644 index 000000000..0a44d5375 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java @@ -0,0 +1,410 @@ +/* + * Copyright 2017 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.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.springframework.cassandra.core.QueryOptionsUtil; +import org.springframework.cassandra.core.WriteOptions; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.convert.QueryMapper; +import org.springframework.data.cassandra.convert.UpdateMapper; +import org.springframework.data.cassandra.core.query.Columns.ColumnSelector; +import org.springframework.data.cassandra.core.query.Columns.FunctionCall; +import org.springframework.data.cassandra.core.query.Columns.Selector; +import org.springframework.data.cassandra.core.query.CriteriaDefinition; +import org.springframework.data.cassandra.core.query.CriteriaDefinition.Predicate; +import org.springframework.data.cassandra.core.query.Filter; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.data.cassandra.core.query.Update.AddToMapOp; +import org.springframework.data.cassandra.core.query.Update.AddToOp; +import org.springframework.data.cassandra.core.query.Update.AddToOp.Mode; +import org.springframework.data.cassandra.core.query.Update.AssignmentOp; +import org.springframework.data.cassandra.core.query.Update.IncrOp; +import org.springframework.data.cassandra.core.query.Update.RemoveOp; +import org.springframework.data.cassandra.core.query.Update.SetAtIndexOp; +import org.springframework.data.cassandra.core.query.Update.SetAtKeyOp; +import org.springframework.data.cassandra.core.query.Update.SetOp; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.util.Assert; + +import com.datastax.driver.core.RegularStatement; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.querybuilder.Assignment; +import com.datastax.driver.core.querybuilder.Clause; +import com.datastax.driver.core.querybuilder.Delete; +import com.datastax.driver.core.querybuilder.Ordering; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Select.Selection; +import com.datastax.driver.core.querybuilder.Select.SelectionOrAlias; +import com.google.common.primitives.Ints; + +/** + * Statement factory to render {@link Statement} from {@link Query} and {@link Update} objects. + * + * @author Mark Paluch + * @since 2.0 + */ +public class StatementFactory { + + private final QueryMapper queryMapper; + + private final UpdateMapper updateMapper; + + /** + * Create {@link StatementFactory} given {@link UpdateMapper}. + * + * @param updateMapper must not be {@literal null}. + */ + public StatementFactory(UpdateMapper updateMapper) { + this(updateMapper, updateMapper); + } + + /** + * Create {@link StatementFactory} given {@link QueryMapper} and {@link UpdateMapper}. + * + * @param queryMapper must not be {@literal null}. + * @param updateMapper must not be {@literal null}. + */ + public StatementFactory(QueryMapper queryMapper, UpdateMapper updateMapper) { + + Assert.notNull(queryMapper, "QueryMapper must not be null"); + Assert.notNull(updateMapper, "UpdateMapper must not be null"); + + this.queryMapper = queryMapper; + this.updateMapper = updateMapper; + } + + /** + * Create a {@literal SELECT} statement by mapping {@link Query} to {@link Select}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + */ + public RegularStatement select(Query query, CassandraPersistentEntity entity) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + List selectors = queryMapper.getMappedSelectors(query.getColumns(), entity); + + Filter filter = queryMapper.getMappedObject(query, entity); + Sort sort = query.getSort() != null ? queryMapper.getMappedSort(query.getSort(), entity) : null; + + Select select = select(selectors, entity.getTableName(), filter, sort); + + query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(select, queryOptions)); + + if (query.getLimit() > 0) { + select.limit(Ints.checkedCast(query.getLimit())); + } + + if (query.isAllowFiltering()) { + select.allowFiltering(); + } + + query.getPagingState().ifPresent(select::setPagingState); + + return select; + } + + private static Select select(List selectors, CqlIdentifier from, Filter filter, Sort sort) { + + Select select; + + if (selectors.isEmpty()) { + select = QueryBuilder.select().all().from(from.toCql()); + } else { + + Selection selection = QueryBuilder.select(); + selectors.forEach(selector -> { + selector.getAlias().map(CqlIdentifier::toCql).ifPresent(getSelection(selection, selector)::as); + }); + select = selection.from(from.toCql()); + } + + for (CriteriaDefinition criteriaDefinition : filter) { + select.where(toClause(criteriaDefinition)); + } + + if (sort != null) { + + List orderings = new ArrayList<>(); + for (Order order : sort) { + + if (order.isAscending()) { + orderings.add(QueryBuilder.asc(order.getProperty())); + } else { + orderings.add(QueryBuilder.desc(order.getProperty())); + } + } + + if (!orderings.isEmpty()) { + select.orderBy(orderings.toArray(new Ordering[orderings.size()])); + } + } + + return select; + } + + private static SelectionOrAlias getSelection(Selection selection, Selector selector) { + + if (selector instanceof FunctionCall) { + + Object[] objects = ((FunctionCall) selector).getParameters().stream().map(o -> { + + if (o instanceof ColumnSelector) { + return QueryBuilder.column(((ColumnSelector) o).getExpression()); + } + + return o; + + }).toArray(); + + return selection.fcall(selector.getExpression(), objects); + } + + return selection.column(selector.getExpression()); + } + + /** + * Create an {@literal UPDATE} statement by mapping {@link Query} to {@link Update}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + */ + public RegularStatement update(Query query, Update updateObj, CassandraPersistentEntity entity) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + Update mappedUpdate = updateMapper.getMappedObject(updateObj, entity); + Filter filter = queryMapper.getMappedObject(query, entity); + + com.datastax.driver.core.querybuilder.Update update = update(entity.getTableName(), mappedUpdate, filter); + + query.getQueryOptions().ifPresent(queryOptions -> { + + if (queryOptions instanceof WriteOptions) { + QueryOptionsUtil.addWriteOptions(update, (WriteOptions) queryOptions); + } else { + QueryOptionsUtil.addQueryOptions(update, queryOptions); + } + }); + + query.getPagingState().ifPresent(update::setPagingState); + + return update; + } + + private static com.datastax.driver.core.querybuilder.Update update(CqlIdentifier table, Update mappedUpdate, + Filter filter) { + + com.datastax.driver.core.querybuilder.Update update = QueryBuilder.update(table.toCql()); + + for (AssignmentOp assignmentOp : mappedUpdate.getUpdateOperations()) { + update.with(getAssignment(assignmentOp)); + } + + for (CriteriaDefinition criteriaDefinition : filter) { + update.where(toClause(criteriaDefinition)); + } + + return update; + } + + private static Assignment getAssignment(AssignmentOp assignmentOp) { + + if (assignmentOp instanceof SetOp) { + return getAssignment((SetOp) assignmentOp); + } + + if (assignmentOp instanceof RemoveOp) { + return getAssignment((RemoveOp) assignmentOp); + } + + if (assignmentOp instanceof IncrOp) { + return getAssignment((IncrOp) assignmentOp); + } + + if (assignmentOp instanceof AddToOp) { + return getAssignment((AddToOp) assignmentOp); + } + + if (assignmentOp instanceof AddToMapOp) { + return getAssignment((AddToMapOp) assignmentOp); + } + + throw new IllegalArgumentException(String.format("UpdateOp %s not supported", assignmentOp)); + } + + private static Assignment getAssignment(IncrOp incrOp) { + + if (incrOp.getValue().intValue() > 0) { + return QueryBuilder.incr(incrOp.getColumnName().toCql(), Math.abs(incrOp.getValue().intValue())); + } + + return QueryBuilder.decr(incrOp.getColumnName().toCql(), Math.abs(incrOp.getValue().intValue())); + } + + private static Assignment getAssignment(SetOp updateOp) { + + if (updateOp instanceof SetAtIndexOp) { + + SetAtIndexOp op = (SetAtIndexOp) updateOp; + + return QueryBuilder.setIdx(op.getColumnName().toCql(), op.getIndex(), op.getValue()); + } + + if (updateOp instanceof SetAtKeyOp) { + + SetAtKeyOp op = (SetAtKeyOp) updateOp; + return QueryBuilder.put(op.getColumnName().toCql(), op.getKey(), op.getValue()); + } + + return QueryBuilder.set(updateOp.getColumnName().toCql(), updateOp.getValue()); + } + + private static Assignment getAssignment(RemoveOp updateOp) { + + if (updateOp.getValue() instanceof Set) { + return QueryBuilder.removeAll(updateOp.getColumnName().toCql(), (Set) updateOp.getValue()); + } + + if (updateOp.getValue() instanceof List) { + return QueryBuilder.discardAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue()); + } + + return QueryBuilder.remove(updateOp.getColumnName().toCql(), updateOp.getValue()); + } + + @SuppressWarnings("unchecked") + private static Assignment getAssignment(AddToOp updateOp) { + + if (updateOp.getValue() instanceof Set) { + return QueryBuilder.addAll(updateOp.getColumnName().toCql(), (Set) updateOp.getValue()); + } + + if (updateOp.getMode() == Mode.PREPEND) { + return QueryBuilder.prependAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue()); + } + + return QueryBuilder.appendAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue()); + } + + private static Assignment getAssignment(AddToMapOp updateOp) { + return QueryBuilder.putAll(updateOp.getColumnName().toCql(), updateOp.getValue()); + } + + /** + * Create a {@literal DELETE} statement by mapping {@link Query} to {@link Delete}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + */ + public RegularStatement delete(Query query, CassandraPersistentEntity entity) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + List columnNames = queryMapper.getMappedColumnNames(query.getColumns(), entity); + Filter filter = queryMapper.getMappedObject(query, entity); + + Delete delete = delete(columnNames, entity.getTableName(), filter); + + query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(delete, queryOptions)); + + query.getPagingState().ifPresent(delete::setPagingState); + + return delete; + } + + private static Delete delete(List columnNames, CqlIdentifier from, Filter filter) { + + Delete select; + + if (columnNames.isEmpty()) { + select = QueryBuilder.delete().all().from(from.toCql()); + } else { + Delete.Selection selection = QueryBuilder.delete(); + columnNames.forEach(selection::column); + select = selection.from(from.toCql()); + } + + for (CriteriaDefinition criteriaDefinition : filter) { + select.where(toClause(criteriaDefinition)); + } + + return select; + } + + private static Clause toClause(CriteriaDefinition criteriaDefinition) { + + Predicate predicate = criteriaDefinition.getPredicate(); + String columnName = criteriaDefinition.getColumnName().toCql(); + + switch (predicate.getOperator().toString()) { + + case "=": + return QueryBuilder.eq(columnName, predicate.getValue()); + + case ">": + return QueryBuilder.gt(columnName, predicate.getValue()); + + case ">=": + return QueryBuilder.gte(columnName, predicate.getValue()); + + case "<": + return QueryBuilder.lt(columnName, predicate.getValue()); + + case "<=": + return QueryBuilder.lte(columnName, predicate.getValue()); + + case "IN": + + if (predicate.getValue() instanceof List) { + return QueryBuilder.in(columnName, (List) predicate.getValue()); + } + + if (predicate.getValue().getClass().isArray()) { + return QueryBuilder.in(columnName, (Object[]) predicate.getValue()); + } + + return QueryBuilder.in(columnName, predicate.getValue()); + + case "LIKE": + return QueryBuilder.like(columnName, predicate.getValue()); + + case "CONTAINS": + return QueryBuilder.contains(columnName, predicate.getValue()); + + case "CONTAINS KEY": + return QueryBuilder.containsKey(columnName, predicate.getValue()); + } + + throw new IllegalArgumentException( + String.format("Criteria %s %s %s not supported", columnName, predicate.getOperator(), predicate.getValue())); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/ColumnName.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/ColumnName.java new file mode 100644 index 000000000..83677faf0 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/ColumnName.java @@ -0,0 +1,198 @@ +/* + * Copyright 2017 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.core.query; + +import java.util.Optional; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; + +/** + * Value object representing a column name. Column names can be expressed either through {@link CqlIdentifier} or a + * {@link String} literal. Using a String literal preserves case and is suitable to reference properties. + *

+ * Equality and hash code are based on {@link #toCql()} representation. + *

+ * Implementing classes must provide either {@link #getColumnName()} or {@link #getCqlIdentifier()}. + * + * @author Mark Paluch + * @since 2.0 + */ +public abstract class ColumnName { + + /** + * Create a {@link ColumnName} given {@link CqlIdentifier}. The resulting instance uses CQL identifier rules to + * identify column names (quoting, case-sensitivity). + * + * @param cqlIdentifier must not be {@literal null}. + * @return the {@link ColumnName} for {@link CqlIdentifier} + * @see CqlIdentifier + */ + public static ColumnName from(CqlIdentifier cqlIdentifier) { + + Assert.notNull(cqlIdentifier, "Column name must not be null"); + + return new CqlIdentifierColumnName(cqlIdentifier); + } + + /** + * Create a {@link ColumnName} given a string {@code columnName}. The resulting instance uses String rules to identify + * column names (case-sensitivity). + * + * @param columnName must not be {@literal null} or empty. + * @return the {@link ColumnName} for {@link CqlIdentifier} + */ + public static ColumnName from(String columnName) { + + Assert.notNull(columnName, "Column name must not be null"); + Assert.hasText(columnName, "Column name must not be empty"); + + return new StringColumnName(columnName); + } + + /** + * @return the optional column name. + */ + public abstract Optional getColumnName(); + + /** + * @return the optional {@link CqlIdentifier}. + */ + public abstract Optional getCqlIdentifier(); + + /** + * Represent the column name as CQL. + * + * @return CQL representation of the column name. + */ + public abstract String toCql(); + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Criteria#equals(java.lang.Object) + */ + @Override + public boolean equals(Object o) { + + if (this == o) + return true; + if (!(o instanceof ColumnName)) + return false; + + ColumnName that = (ColumnName) o; + + return toCql().equals(that.toCql()); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return 31 + toCql().hashCode(); + } + + /** + * {@link String}-based column name representation. Preserves letter casing. + * + * @author Mark Paluch + */ + static class StringColumnName extends ColumnName { + + private final String columnName; + + StringColumnName(String columnName) { + this.columnName = columnName; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return columnName; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#getColumnName() + */ + @Override + public Optional getColumnName() { + return Optional.of(columnName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#getCqlIdentifier() + */ + @Override + public Optional getCqlIdentifier() { + return Optional.empty(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#toCql() + */ + @Override + public String toCql() { + return columnName; + } + } + + /** + * {@link CqlIdentifier}-based column name representation. Follows {@link CqlIdentifier} comparison rules. + * + * @author Mark Paluch + */ + static class CqlIdentifierColumnName extends ColumnName { + + private final CqlIdentifier cqlIdentifier; + + CqlIdentifierColumnName(CqlIdentifier cqlIdentifier) { + this.cqlIdentifier = cqlIdentifier; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return cqlIdentifier.toString(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#getColumnName() + */ + @Override + public Optional getColumnName() { + return Optional.empty(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#getCqlIdentifier() + */ + @Override + public Optional getCqlIdentifier() { + return Optional.of(cqlIdentifier); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.ColumnName#toCql() + */ + @Override + public String toCql() { + return cqlIdentifier.toCql(); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Columns.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Columns.java new file mode 100644 index 000000000..b1eac6bc1 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Columns.java @@ -0,0 +1,464 @@ +/* + * Copyright 2017 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.core.query; + +import lombok.EqualsAndHashCode; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Value object to abstract column names involved in a CQL query. Columns can be constructed from an array of names and + * included using a {@link Selector}. + * + * @author Mark Paluch + * @since 2.0 + * @see CqlIdentifier + * @see ColumnName + * @see Selector + * @see ColumnSelector + * @see FunctionCall + */ +public class Columns implements Iterable { + + private final Map columns; + + private Columns(Map columns) { + this.columns = Collections.unmodifiableMap(columns); + } + + /** + * Create an empty {@link Columns} instance without any columns. + * + * @return an empty {@link Columns} instance. + */ + public static Columns empty() { + return new Columns(Collections.emptyMap()); + } + + /** + * Create a {@link Columns} given {@code columnNames}. Individual column names can be either quoted or unquoted. + * + * @param columnNames must not be {@literal null}. + * @return the {@link Columns} object for {@code columnNames}. + */ + public static Columns from(String... columnNames) { + + Assert.notNull(columnNames, "Column names must not be null"); + + Map columns = new HashMap<>(columnNames.length, 1); + + Arrays.stream(columnNames) + .forEach(columnName -> columns.put(ColumnName.from(columnName), ColumnSelector.from(columnName))); + + return new Columns(columns); + } + + /** + * Create a {@link Columns} given {@code columnNames}. + * + * @param columnNames must not be {@literal null}. + * @return the {@link Columns} object for {@code columnNames}. + */ + public static Columns from(CqlIdentifier... columnNames) { + + Assert.notNull(columnNames, "Column names must not be null"); + + Map columns = new HashMap<>(columnNames.length, 1); + + Arrays.stream(columnNames).forEach(cqlId -> columns.put(ColumnName.from(cqlId), ColumnSelector.from(cqlId))); + + return new Columns(columns); + } + + /** + * Include column {@code columnName} to the selection. Column inclusion overrides an existing selection for the column + * name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the included {@code columnName}. + */ + public Columns include(String columnName) { + return select(columnName, ColumnSelector.from(columnName)); + } + + /** + * Include column {@code columnName} to the selection. Column inclusion overrides an existing selection for the column + * name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the included {@code columnName}. + */ + public Columns include(CqlIdentifier columnName) { + return select(columnName, ColumnSelector.from(columnName)); + } + + /** + * Include column {@code columnName} as TTL value in the selection. This column selection overrides an existing + * selection for the column name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the TTL for {@code columnName}. + */ + public Columns ttl(String columnName) { + return select(columnName, FunctionCall.from("TTL", ColumnSelector.from(columnName))); + } + + /** + * Include column {@code columnName} as TTL value in the selection. This column selection overrides an existing + * selection for the column name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the TTL for {@code columnName}. + */ + public Columns ttl(CqlIdentifier columnName) { + return select(columnName, FunctionCall.from("TTL", ColumnSelector.from(columnName))); + } + + /** + * Include column {@code columnName} with {@link Selector}. This column selection overrides an existing selection for + * the column name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the selected {@code columnName}. + */ + public Columns select(String columnName, Selector selector) { + + Assert.notNull(columnName, "Column name must not be null"); + + Map result = new LinkedHashMap<>(this.columns); + result.put(ColumnName.from(columnName), selector); + + return new Columns(result); + } + + /** + * Include column {@code columnName} with {@link Selector}. This column selection overrides an existing selection for + * the column name. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Columns} object containing all column definitions and the selected {@code columnName}. + */ + public Columns select(CqlIdentifier columnName, Selector selector) { + + Assert.notNull(columnName, "Column name must not be null"); + + Map result = new LinkedHashMap<>(this.columns); + result.put(ColumnName.from(columnName), selector); + + return new Columns(result); + } + + /** + * @return {@literal true} if no columns were specified and this {@link Columns} object is empty. + */ + public boolean isEmpty() { + return this.columns.isEmpty(); + } + + /** + * Returns a new {@link Columns} consisting of the {@link ColumnName}s of the current {@link Columns} combined with + * the given ones. Existing {@link ColumnName}s are overwritten if specified within {@code columns}. + * + * @param columns can be {@literal null}. + * @return a new {@link Columns} with the merged result of the configured and given {@link Columns}. + */ + public Columns and(Columns columns) { + + Map result = new LinkedHashMap<>(this.columns); + + result.putAll(columns.columns); + + return new Columns(result); + } + + /* (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return this.columns.keySet().iterator(); + } + + /** + * @param columnName must not be {@literal null}. + * @return the {@link Optional} {@link Selector} for {@link ColumnName}. + */ + public Optional getSelector(ColumnName columnName) { + + Assert.notNull(columnName, "ColumnName must not be null"); + + return Optional.ofNullable(this.columns.get(columnName)); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object object) { + + if (this == object) { + return true; + } + + if (!(object instanceof Columns)) { + return false; + } + + Columns that = (Columns) object; + + return this.columns.equals(that.columns); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += 31 * ObjectUtils.nullSafeHashCode(this.columns); + + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + Iterator> iterator = this.columns.entrySet().iterator(); + StringBuilder builder = toString(iterator); + + if (builder.length() == 0) { + return "*"; + } + + return builder.toString(); + } + + private StringBuilder toString(Iterator> iterator) { + + StringBuilder builder = new StringBuilder(); + boolean first = true; + + while (iterator.hasNext()) { + + Entry entry = iterator.next(); + + Selector expression = entry.getValue(); + + if (first) { + first = false; + } else { + builder.append(", "); + } + + builder.append(expression.toString()); + } + + return builder; + } + + /** + * Strategy interface to render a column selection. + * + * @author Mark Paluch + */ + public interface Selector { + + String getExpression(); + + Optional getAlias(); + } + + /** + * Column selection. + * + * @author Mark Paluch + */ + @EqualsAndHashCode + public static class ColumnSelector implements Selector { + + private final ColumnName columnName; + private final Optional alias; + + ColumnSelector(ColumnName columnName) { + + Assert.notNull(columnName, "ColumnName must not be null"); + + this.columnName = columnName; + this.alias = Optional.empty(); + } + + ColumnSelector(ColumnName columnName, CqlIdentifier alias) { + + Assert.notNull(columnName, "ColumnName must not be null"); + Assert.notNull(alias, "Alias must not be null"); + + this.columnName = columnName; + this.alias = Optional.of(alias); + } + + /** + * Create a {@link ColumnSelector} given {@link ColumnName}. + */ + public static ColumnSelector from(ColumnName columnName) { + return new ColumnSelector(columnName); + } + + /** + * Create a {@link ColumnSelector} given {@link CqlIdentifier}. + */ + public static ColumnSelector from(CqlIdentifier columnName) { + return new ColumnSelector(ColumnName.from(columnName)); + } + + /** + * Create a {@link ColumnSelector} given a plain {@code columnName}. + */ + public static ColumnSelector from(String columnName) { + return new ColumnSelector(ColumnName.from(columnName)); + } + + /** + * Create a {@link ColumnSelector} for the current {@link #getExpression() expression} aliased as {@code alias}. + * + * @param alias must not be {@literal null} or empty. + * @return the aliased {@link ColumnSelector}. + */ + public ColumnSelector as(String alias) { + return as(CqlIdentifier.cqlId(alias)); + } + + /** + * Create a {@link ColumnSelector} for the current {@link #getExpression() expression} aliased as {@code alias}. + * + * @param alias must not be {@literal null}. + * @return the aliased {@link ColumnSelector}. + */ + public ColumnSelector as(CqlIdentifier alias) { + return new ColumnSelector(columnName, alias); + } + + public String getExpression() { + return columnName.toCql(); + } + + public Optional getAlias() { + return alias; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return getAlias().map(cqlIdentifier -> String.format("%s AS %s", getExpression(), cqlIdentifier.toCql())) + .orElseGet(this::getExpression); + } + } + + /** + * Function call selector with alias support. + */ + @EqualsAndHashCode + public static class FunctionCall implements Selector { + + private final String expression; + private final List params; + private final Optional alias; + + FunctionCall(String expression, List params) { + + this.expression = expression; + this.params = params; + this.alias = Optional.empty(); + } + + private FunctionCall(String expression, List params, CqlIdentifier alias) { + + this.expression = expression; + this.params = params; + this.alias = Optional.of(alias); + } + + public static FunctionCall from(String expression, Object... params) { + return new FunctionCall(expression, Arrays.asList(params)); + } + + /** + * Create a {@link FunctionCall} for the current {@link #getExpression() expression} aliased as {@code alias}. + * + * @param alias must not be {@literal null} or empty. + * @return the aliased {@link ColumnSelector}. + */ + public FunctionCall as(String alias) { + return as(CqlIdentifier.cqlId(alias)); + } + + /** + * Create a {@link FunctionCall} for the current {@link #getExpression() expression} aliased as {@code alias}. + * + * @param alias must not be {@literal null}. + * @return the aliased {@link ColumnSelector}. + */ + public FunctionCall as(CqlIdentifier alias) { + return new FunctionCall(expression, params, alias); + } + + @Override + public String getExpression() { + return expression; + } + + @Override + public Optional getAlias() { + return alias; + } + + public List getParameters() { + return params; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Columns.Column#toString() + */ + @Override + public String toString() { + + String params = StringUtils.collectionToDelimitedString(getParameters(), ", "); + + return getAlias() + .map(cqlIdentifier -> String.format("%s(%s) AS %s", getExpression(), params, cqlIdentifier.toCql())) + .orElseGet(() -> String.format("%s(%s)", getExpression(), params)); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java new file mode 100644 index 000000000..64ccb82b4 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java @@ -0,0 +1,287 @@ +/* + * Copyright 2017 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.core.query; + +import static org.springframework.util.ObjectUtils.*; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.Assert; + +/** + * Basic class for creating queries. It follows a fluent API style so that you can easily create a + * {@link CriteriaDefinition}. Static import of the 'Criteria.where' method will improve readability. + * + * @author Mark Paluch + * @since 2.0 + */ +public class Criteria implements CriteriaDefinition { + + private final ColumnName columnName; + + private Predicate predicate; + + private Criteria(ColumnName columnName, Predicate predicate) { + + this(columnName); + + Assert.notNull(predicate, "Predicate must not be null"); + + this.predicate = predicate; + } + + /** + * Create an empty {@link Criteria} given a {@link ColumnName}. + */ + protected Criteria(ColumnName columnName) { + + Assert.notNull(columnName, "ColumnName must not be null"); + + this.columnName = columnName; + } + + /** + * Static factory method to create a {@link Criteria} using the provided {@code columnName}. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Criteria} for {@code columnName}. + */ + public static Criteria where(String columnName) { + return new Criteria(ColumnName.from(columnName)); + } + + /** + * Static factory method to create a {@link Criteria} using the provided {@code columnName}. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Criteria} for {@code columnName}. + */ + public static Criteria of(ColumnName columnName, Predicate predicate) { + + Assert.notNull(columnName, "ColumnName must not be null"); + Assert.notNull(predicate, "Predicate must not be null"); + + return new Criteria(columnName, predicate); + } + + /** + * Create a criterion using equality. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition is(Object value) { + + this.predicate = new Predicate(Operators.EQ, value); + return this; + } + + /** + * Create a criterion using the {@literal >} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition lt(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.LT, value); + return this; + } + + /** + * Create a criterion using the {@literal >=} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition lte(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.LTE, value); + return this; + } + + /** + * Create a criterion using the {@literal <} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition gt(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.GT, value); + return this; + } + + /** + * Create a criterion using the {@literal <=} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition gte(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.GTE, value); + return this; + } + + /** + * Create a criterion using the {@literal IN} operator. + * + * @param values the values to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition in(Object... values) { + + Assert.notNull(values, "Value must not be null"); + + if (values.length > 1 && values[1] instanceof Collection) { + throw new InvalidDataAccessApiUsageException( + "You can only pass in one argument of type " + values[1].getClass().getName()); + } + + return in(Arrays.asList(values)); + } + + /** + * Create a criterion using the {@literal IN} operator. + * + * @param values the collection of values to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition in(Collection values) { + + Assert.notNull(values, "Value must not be null"); + + this.predicate = new Predicate(Operators.IN, values); + return this; + } + + /** + * Create a criterion using the {@literal LIKE} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition like(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.LIKE, value); + return this; + } + + /** + * Create a criterion using the {@literal CONTAINS} operator. + * + * @param value the value to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition contains(Object value) { + + Assert.notNull(value, "Value must not be null"); + + this.predicate = new Predicate(Operators.CONTAINS, value); + return this; + } + + /** + * Create a criterion using the {@literal CONTAINS KEY} operator. + * + * @param key the key to match against. + * @return {@literal this} {@link Criteria} object. + */ + public CriteriaDefinition containsKey(Object key) { + + Assert.notNull(key, "Value must not be null"); + + this.predicate = new Predicate(Operators.CONTAINS_KEY, key); + return this; + } + + /** + * @return the {@link ColumnName}. + */ + public ColumnName getColumnName() { + return this.columnName; + } + + /** + * @return the {@link Predicate}. + */ + public Predicate getPredicate() { + return predicate; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof Criteria)) { + return false; + } + + Criteria that = (Criteria) obj; + + return simpleCriteriaEquals(this, that); + } + + protected boolean simpleCriteriaEquals(CriteriaDefinition left, CriteriaDefinition right) { + + boolean keyEqual = left.getColumnName() == null ? right.getColumnName() == null + : left.getColumnName().equals(right.getColumnName()); + boolean criteriaEqual = left.getPredicate().equals(right.getPredicate()); + + return keyEqual && criteriaEqual; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += nullSafeHashCode(columnName); + result += nullSafeHashCode(predicate); + + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return SerializationUtils.serializeToCqlSafely(this); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/CriteriaDefinition.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/CriteriaDefinition.java new file mode 100644 index 000000000..33439015e --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/CriteriaDefinition.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017 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.core.query; + +import lombok.EqualsAndHashCode; + +import org.springframework.util.Assert; + +/** + * Criteria definition for a {@link ColumnName} exposing a {@link Predicate}. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface CriteriaDefinition { + + /** + * Get the identifying {@literal key}. + * + * @return the {@link ColumnName}. + */ + ColumnName getColumnName(); + + /** + * Get {@link Predicate}. + * + * @return the {@link Predicate} + */ + Predicate getPredicate(); + + /** + * Represents an operator associated with its value. + * + * @author Mark Paluch + */ + @EqualsAndHashCode + class Predicate { + + private final Operator operator; + + private final Object value; + + /** + * Create a new {@link Predicate} given {@code operator} and {@code value}. + * + * @param operator must not be {@literal null}. + * @param value the match value. + */ + public Predicate(Operator operator, Object value) { + + Assert.notNull(operator, "Operator must not be null"); + + this.operator = operator; + this.value = value; + } + + /** + * @return the operator, such as {@literal =}, {@literal >=}, {@literal LIKE}. + */ + public Operator getOperator() { + return operator; + } + + /** + * @return the match value. + */ + public Object getValue() { + return value; + } + } + + /** + * Strategy interface to represent a CQL predicate operator. + */ + interface Operator { + + /** + * @return the String representation of the operator. + */ + String toString(); + } + + /** + * Commonly used CQL operators. + */ + enum Operators implements Operator { + + EQ("="), GT(">"), GTE(">="), LT("<"), LTE("<="), IN("IN"), CONTAINS("CONTAINS"), CONTAINS_KEY("CONTAINS KEY"), LIKE( + "LIKE"); + + private final String operator; + + Operators(String operator) { + this.operator = operator; + } + + @Override + public String toString() { + return operator; + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/DefaultFilter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/DefaultFilter.java new file mode 100644 index 000000000..5e239e529 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/DefaultFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 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.core.query; + +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Default implementation of {@link Filter}. + * + * @author Mark Paluch + * @since 2.0 + */ +class DefaultFilter implements Filter { + + private final Iterable criteriaDefinitions; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + DefaultFilter(Iterable criteriaDefinitions) { + this.criteriaDefinitions = (Iterable) criteriaDefinitions; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Filter#getCriteriaDefinitions() + */ + @Override + public Iterable getCriteriaDefinitions() { + return criteriaDefinitions; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return StreamSupport.stream(this.spliterator(), false) // + .map(SerializationUtils::serializeToCqlSafely) // + .collect(Collectors.joining(" AND ")); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Filter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Filter.java new file mode 100644 index 000000000..cf61aead2 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Filter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017 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.core.query; + +import java.util.Arrays; +import java.util.Iterator; + +import org.springframework.util.Assert; + +/** + * Filter consisting of {@link CriteriaDefinition}s to be used with {@literal SELECT}, {@literal UPDATE} and + * {@literal DELETE} queries. A {@link Filter} describes the matched set of rows to execute a particular operation. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface Filter extends Iterable { + + /** + * @return the {@link CriteriaDefinition}s. + */ + Iterable getCriteriaDefinitions(); + + /** + * Create a simple {@link Filter} given {@link CriteriaDefinition}s. + * + * @param criteriaDefinitions must not be {@literal null}. + * @return the {@link Filter} object for {@link CriteriaDefinition}s. + */ + static Filter from(CriteriaDefinition... criteriaDefinitions) { + + Assert.notNull(criteriaDefinitions, "CriteriaDefinitions must not be null"); + + return from(Arrays.asList(criteriaDefinitions)); + } + + /** + * Create a simple {@link Filter} given {@link CriteriaDefinition}s. + * + * @param criteriaDefinitions must not be {@literal null}. + * @return the {@link Filter} object for {@link CriteriaDefinition}s. + */ + static Filter from(Iterable criteriaDefinitions) { + + Assert.notNull(criteriaDefinitions, "CriteriaDefinitions must not be null"); + + return new DefaultFilter(criteriaDefinitions); + } + + /* (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + default Iterator iterator() { + return getCriteriaDefinitions().iterator(); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Query.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Query.java new file mode 100644 index 000000000..336e42d65 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Query.java @@ -0,0 +1,331 @@ +/* + * Copyright 2017 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.core.query; + +import static org.springframework.util.ObjectUtils.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.cassandra.core.QueryOptions; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.util.Assert; + +import com.datastax.driver.core.PagingState; + +/** + * Query object representing {@link CriteriaDefinition}s, {@link Columns}, {@link Sort}, {@link PagingState} and + * {@link QueryOptions} for a CQL query. {@link Query} is created with a fluent API creating immutable objects. + * + * @author Mark Paluch + * @since 2.0 + */ +public class Query implements Filter { + + private final List criteriaDefinitions; + + private final Columns columns; + + private final Sort sort; + + private final Optional pagingState; + + private final Optional queryOptions; + + private final Optional limit; + + private final boolean allowFiltering; + + private Query(List criteriaDefinitions, Columns columns, Sort sort, + Optional pagingState, Optional queryOptions, Optional limit, + boolean allowFiltering) { + + this.criteriaDefinitions = criteriaDefinitions; + this.columns = columns; + this.sort = sort; + this.pagingState = pagingState; + this.queryOptions = queryOptions; + this.limit = limit; + this.allowFiltering = allowFiltering; + } + + /** + * Static factory method to create an empty {@link Query} + * + * @return the new {@link Query}. + */ + public static Query empty() { + return new Query(Collections.emptyList(), Columns.empty(), Sort.unsorted(), Optional.empty(), Optional.empty(), + Optional.empty(), false); + } + + /** + * Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}. + * + * @param criteriaDefinitions must not be {@literal null}. + * @return the {@link Query} for {@link CriteriaDefinition}s. + */ + public static Query query(CriteriaDefinition... criteriaDefinitions) { + + Assert.notNull(criteriaDefinitions, "CriteriaDefinitions must not be null"); + + return query(Arrays.asList(criteriaDefinitions)); + } + + /** + * Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}. + * + * @param criteriaDefinitions must not be {@literal null}. + * @return the {@link Query} for {@link CriteriaDefinition}s. + */ + public static Query query(Iterable criteriaDefinitions) { + + Assert.notNull(criteriaDefinitions, "CriteriaDefinitions must not be null"); + + List collect = StreamSupport.stream(criteriaDefinitions.spliterator(), false) + .collect(Collectors.toList()); + + return new Query(collect, Columns.empty(), Sort.unsorted(), Optional.empty(), Optional.empty(), Optional.empty(), + false); + } + + /** + * Add the given {@link CriteriaDefinition} to the current {@link Query}. + * + * @param criteriaDefinition must not be {@literal null}. + * @return a new {@link Query} object containing the former settings with {@link CriteriaDefinition} applied. + */ + public Query and(CriteriaDefinition criteriaDefinition) { + + Assert.notNull(criteriaDefinition, "Criteria must not be null"); + + List criteriaDefinitions = new ArrayList<>(this.criteriaDefinitions.size() + 1); + criteriaDefinitions.addAll(this.criteriaDefinitions); + + if (!criteriaDefinitions.contains(criteriaDefinition)) { + criteriaDefinitions.add(criteriaDefinition); + } + + return new Query(criteriaDefinitions, columns, sort, pagingState, queryOptions, limit, allowFiltering); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Filter#getCriteriaDefinitions() + */ + @Override + public Iterable getCriteriaDefinitions() { + return Collections.unmodifiableCollection(criteriaDefinitions); + } + + /** + * Add {@link Columns} to the {@link Query} instance. Existing definitions are merged or overwritten for overriding + * {@link ColumnName}s in {@code columns}. + * + * @param columns must not be {@literal null}. + * @return a new {@link Query} object containing the former settings with {@link Columns} applied. + */ + public Query columns(Columns columns) { + + Assert.notNull(columns, "Columns must not be null"); + + return new Query(criteriaDefinitions, this.columns.and(columns), sort, pagingState, queryOptions, limit, + allowFiltering); + } + + /** + * @return the query {@link Columns}. + */ + public Columns getColumns() { + return columns; + } + + /** + * Add a {@link Sort} to the {@link Query} instance. + * + * @param sort must not be {@literal null}. + * @return a new {@link Query} object containing the former settings with {@link Sort} applied. + */ + public Query sort(Sort sort) { + + Assert.notNull(sort, "Sort must not be null"); + + for (Order order : sort) { + if (order.isIgnoreCase()) { + throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! " + + "Apache Cassandra does not support sorting ignoring case currently!", order.getProperty())); + } + } + + return new Query(criteriaDefinitions, columns, this.sort.and(sort), pagingState, queryOptions, limit, + allowFiltering); + } + + /** + * @return the query {@link Sort} object. + */ + public Sort getSort() { + return sort; + } + + /** + * Set the {@link PagingState} to skip rows. + * + * @param pagingState must not be {@literal null}. + * @return a new {@link Query} object containing the former settings with {@link PagingState} applied. + */ + public Query pagingState(PagingState pagingState) { + + Assert.notNull(pagingState, "PagingState must not be null"); + + return new Query(criteriaDefinitions, columns, sort, Optional.of(pagingState), queryOptions, limit, allowFiltering); + } + + /** + * @return the optional {@link PagingState}. + */ + public Optional getPagingState() { + return pagingState; + } + + /** + * Set the {@link QueryOptions}. + * + * @param queryOptions must not be {@literal null}. + * @return a new {@link Query} object containing the former settings with {@link QueryOptions} applied. + */ + public Query queryOptions(QueryOptions queryOptions) { + + Assert.notNull(queryOptions, "QueryOptions must not be null"); + + return new Query(criteriaDefinitions, columns, sort, pagingState, Optional.of(queryOptions), limit, allowFiltering); + } + + /** + * @return the optional {@link QueryOptions}. + */ + public Optional getQueryOptions() { + return queryOptions; + } + + /** + * Limit the number of returned rows to {@code limit}. + * + * @param limit + * @return a new {@link Query} object containing the former settings with {@code limit} applied. + */ + public Query limit(long limit) { + return new Query(criteriaDefinitions, columns, sort, pagingState, queryOptions, Optional.of(limit), allowFiltering); + } + + /** + * @return the maximum number of rows to be returned. + */ + public long getLimit() { + return this.limit.orElse(0L); + } + + /** + * Allow filtering with {@code this} {@link Query}. + * + * @return a new {@link Query} object containing the former settings with {@code allowFiltering} applied. + */ + public Query withAllowFiltering() { + + return new Query(criteriaDefinitions, columns, sort, pagingState, queryOptions, limit, true); + } + + /** + * @return {@literal true} to allow filtering. + */ + public boolean isAllowFiltering() { + return allowFiltering; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + return querySettingsEquals((Query) obj); + } + + /** + * Tests whether the settings of the given {@link Query} are equal to this query. + * + * @param that + * @return + */ + protected boolean querySettingsEquals(Query that) { + + boolean criteriaEqual = this.criteriaDefinitions.equals(that.criteriaDefinitions); + boolean columnsEqual = nullSafeEquals(this.columns, that.columns); + boolean sortEqual = nullSafeEquals(this.sort, that.sort); + boolean pagingStateEqual = nullSafeEquals(this.pagingState, that.pagingState); + boolean queryOptionsEqual = nullSafeEquals(this.queryOptions, that.queryOptions); + boolean limitEqual = this.limit == that.limit; + boolean allowFilteringEqual = this.allowFiltering == that.allowFiltering; + + return criteriaEqual && columnsEqual && sortEqual && pagingStateEqual && queryOptionsEqual && limitEqual + && allowFilteringEqual; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += 31 * criteriaDefinitions.hashCode(); + result += 31 * nullSafeHashCode(columns); + result += 31 * nullSafeHashCode(sort); + result += 31 * nullSafeHashCode(pagingState); + result += 31 * nullSafeHashCode(queryOptions); + result += 31 * nullSafeHashCode(limit); + result += (allowFiltering ? 0 : 1); + + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + String query = StreamSupport.stream(this.spliterator(), false) // + .map(SerializationUtils::serializeToCqlSafely) // + .collect(Collectors.joining(" AND ")); + + return String.format("Query: %s, Columns: %s, Sort: %s, Limit: %d", query, getColumns(), getSort(), getLimit()); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/SerializationUtils.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/SerializationUtils.java new file mode 100644 index 000000000..41cb0b551 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/SerializationUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017 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.core.query; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.core.query.CriteriaDefinition.Operator; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.TypeCodec; + +/** + * Utility methods for CQL serialization. + * + * @author Mark Paluch + * @since 2.0 + */ +abstract class SerializationUtils { + + private SerializationUtils() {} + + /** + * Serializes the given object into pseudo-CQL meaning it's trying to create a CQL representation as far as possible + * but falling back to the given object's {@link Object#toString()} method if it's not serializable. Useful for + * printing raw {@link Criteria}s containing complex values before actually converting them into Mongo native types. + * + * @param criteria + * @return + */ + public static String serializeToCqlSafely(CriteriaDefinition criteria) { + + if (criteria == null) { + return null; + } + + CriteriaDefinition.Predicate predicate = criteria.getPredicate(); + return serialize(criteria.getColumnName(), criteria.getPredicate().getOperator()) + .append(serializeToCqlSafely(predicate.getValue())).toString(); + + } + + /** + * Serializes the given object into pseudo-CQL meaning it's trying to create a CQL representation as far as possible + * but falling back to the given object's {@link Object#toString()} method if it's not serializable. Useful for + * printing raw {@link Criteria}s containing complex values before actually converting them into Mongo native types. + * + * @param criteria + * @return + */ + public static String serializeToCqlSafely(Object value) { + + if (value == null) { + return null; + } + + try { + return serialize(value); + } catch (Exception e) { + if (value instanceof Set) { + return toString((Set) value); + } else if (value instanceof Collection) { + return toString((Collection) value); + } else if (value instanceof Map) { + return toString((Map) value); + } else { + return value.toString(); + } + } + } + + private static String serialize(Object value) { + + if (value == null) { + return "null"; + } + + TypeCodec codec = CodecRegistry.DEFAULT_INSTANCE.codecFor(value); + return codec.format(value); + } + + private static StringBuilder serialize(ColumnName key, Operator operator) { + + StringBuilder builder = new StringBuilder(16); + + return builder.append(key).append(' ').append(operator).append(' '); + } + + private static String toString(Map source) { + + return iterableToDelimitedString(source.entrySet(), "{ ", " }", + s -> String.format("%s : %s", serialize(s.getKey()), serialize(s.getValue()))); + } + + private static String toString(Set source) { + return iterableToDelimitedString(source, "{", "}", + (Converter) SerializationUtils::serializeToCqlSafely); + } + + private static String toString(Collection source) { + return iterableToDelimitedString(source, "[", "]", + (Converter) SerializationUtils::serializeToCqlSafely); + } + + /** + * Creates a string representation from the given {@link Iterable} prepending the prefix, applying the given + * {@link Converter} to each element before adding it to the result {@link String}, concatenating each element with + * {@literal ,} and applying the postfix. + */ + private static String iterableToDelimitedString(Iterable source, String prefix, String postfix, + Converter transformer) { + + StringBuilder builder = new StringBuilder(prefix); + Iterator iterator = source.iterator(); + + while (iterator.hasNext()) { + builder.append(transformer.convert(iterator.next())); + if (iterator.hasNext()) { + builder.append(","); + } + } + + return builder.append(postfix).toString(); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Update.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Update.java new file mode 100644 index 000000000..97d861f57 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Update.java @@ -0,0 +1,699 @@ +/* + * Copyright 2017 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.core.query; + +import static org.springframework.data.cassandra.core.query.SerializationUtils.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.cassandra.core.query.Update.AddToOp.Mode; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Update object representing representing a set of update operations. {@link Update} objects can be created in a fluent + * style. Each construction operation creates a new immutable {@link Update} object. + * + *
+ * Update update = Update.empty().set("foo", "bar").addTo("baz").prependAll(listOfValues);
+ * 
+ * + * @author Mark Paluch + * @since 2.0 + */ +public class Update { + + private final Map updateOperations; + + private Update(Map updateOperations) { + this.updateOperations = updateOperations; + } + + /** + * Create an empty {@link Update} object. + * + * @return a new {@link Update}. + */ + public static Update empty() { + return new Update(Collections.emptyMap()); + } + + /** + * Set the {@code columnName} to {@code value}. + * + * @return a new {@link Update}. + */ + public static Update update(String columnName, Object value) { + return empty().set(columnName, value); + } + + /** + * Create a {@link Update} object given a list of {@link AssignmentOp}s. + * + * @param assignmentOps must not be {@literal null}. + */ + public static Update of(Iterable assignmentOps) { + + Assert.notNull(assignmentOps, "Update operations must not be null"); + + Map updateOperations = assignmentOps instanceof Collection + ? new LinkedHashMap<>(((Collection) assignmentOps).size()) : new LinkedHashMap<>(); + + assignmentOps.forEach(assignmentOp -> updateOperations.put(assignmentOp.getColumnName(), assignmentOp)); + + return new Update(updateOperations); + } + + /** + * Set the {@code columnName} to {@code value}. + * + * @param columnName must not be {@literal null}. + * @param value + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update set(String columnName, Object value) { + return add(new SetOp(ColumnName.from(columnName), value)); + } + + /** + * Create a new {@link SetBuilder} to set a collection item for {@code columnName} in a fluent style. + * + * @param columnName must not be {@literal null}. + * @return a new {@link AddToBuilder} to build an set assignment. + */ + public SetBuilder set(String columnName) { + return new DefaultSetBuilder(ColumnName.from(columnName)); + } + + /** + * Create a new {@link AddToBuilder} to add items to a collection for {@code columnName} in a fluent style. + * + * @param columnName must not be {@literal null}. + * @return a new {@link AddToBuilder} to build an add-to assignment. + */ + public AddToBuilder addTo(String columnName) { + return new DefaultAddToBuilder(ColumnName.from(columnName)); + } + + /** + * Remove {@code value} from the collection at {@code columnName}. + * + * @param columnName must not be {@literal null}. + * @param value must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update remove(String columnName, Object value) { + return add(new RemoveOp(ColumnName.from(columnName), Collections.singletonList(value))); + } + + /** + * Cleat the collection at {@code columnName}. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update clear(String columnName) { + return add(new SetOp(ColumnName.from(columnName), Collections.emptyList())); + } + + /** + * Increment the value at {@code columnName} by {@literal 1}. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update increment(String columnName) { + return increment(columnName, 1); + } + + /** + * Increment the value at {@code columnName} by {@code delta}. + * + * @param columnName must not be {@literal null}. + * @param delta increment value. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update increment(String columnName, Number delta) { + return add(new IncrOp(ColumnName.from(columnName), delta)); + } + + /** + * Decrement the value at {@code columnName} by {@literal 1}. + * + * @param columnName must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update decrement(String columnName) { + return decrement(columnName, 1); + } + + /** + * Decrement the value at {@code columnName} by {@code delta}. + * + * @param columnName must not be {@literal null}. + * @param delta decrement value. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + public Update decrement(String columnName, Number delta) { + + if (delta.doubleValue() > 0) { + return add(new IncrOp(ColumnName.from(columnName), -Math.abs(delta.doubleValue()))); + } + + return add(new IncrOp(ColumnName.from(columnName), delta.doubleValue())); + } + + /** + * @return {@link Collection} of update operations. + */ + public Collection getUpdateOperations() { + return Collections.unmodifiableCollection(updateOperations.values()); + } + + private Update add(AssignmentOp assignmentOp) { + + Map map = new LinkedHashMap<>(this.updateOperations.size() + 1); + + map.putAll(this.updateOperations); + map.put(assignmentOp.getColumnName(), assignmentOp); + + return new Update(map); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return StringUtils.collectionToDelimitedString(updateOperations.values(), ", "); + } + + /** + * Builder to add a single element/multiple elements to a collection associated with a {@link ColumnName}. + * + * @author Mark Paluch + */ + public interface AddToBuilder { + + /** + * Prepend the {@code value} to the collection. + * + * @param value must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update prepend(Object value); + + /** + * Prepend all {@code values} to the collection. + * + * @param values must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update prependAll(Object... values); + + /** + * Prepend all {@code values} to the collection. + * + * @param values must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update prependAll(Iterable values); + + /** + * Append the {@code value} to the collection. + * + * @param value must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update append(Object value); + + /** + * Append all {@code values} to the collection. + * + * @param values must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update appendAll(Object... values); + + /** + * Append all {@code values} to the collection. + * + * @param values must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update appendAll(Iterable values); + + /** + * Associate the specified {@code value} with the specified {@code key} in the map. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update entry(Object key, Object value); + + /** + * Associate all entries of the specified {@code map} with the map at {@link ColumnName}. + * + * @param map must not be {@literal null}. + * @return a new {@link Update} object containing the merge result of the existing assignments and the current + * assignment. + */ + Update addAll(Map map); + } + + /** + * Default {@link AddToBuilder} implementation. + */ + private class DefaultAddToBuilder implements AddToBuilder { + + private final ColumnName columnName; + + DefaultAddToBuilder(ColumnName columnName) { + this.columnName = columnName; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prepend(java.lang.Object) + */ + @Override + public Update prepend(Object value) { + return prependAll(Collections.singleton(value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#append(java.lang.Object) + */ + @Override + public Update append(Object value) { + return prependAll(Collections.singleton(value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#entry(java.lang.Object, java.lang.Object) + */ + @Override + public Update entry(Object key, Object value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return addAll(Collections.singletonMap(key, value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#appendAll(java.lang.Object[]) + */ + @Override + public Update appendAll(Object... values) { + + Assert.notNull(values, "Values must not be null"); + + return appendAll(Arrays.asList(values)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#appendAll(java.lang.Iterable) + */ + @Override + public Update appendAll(Iterable values) { + + Assert.notNull(values, "Values must not be null"); + + return add(new AddToOp(columnName, values, Mode.APPEND)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prependAll(java.lang.Object[]) + */ + @Override + public Update prependAll(Object... values) { + + Assert.notNull(values, "Values must not be null"); + + return prependAll(Arrays.asList(values)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prependAll(java.lang.Iterable) + */ + @Override + public Update prependAll(Iterable values) { + + Assert.notNull(values, "Values must not be null"); + + return add(new AddToOp(columnName, values, Mode.PREPEND)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#addAll(java.util.Map) + */ + @Override + public Update addAll(Map map) { + + Assert.notNull(map, "Map must not be null"); + + return add(new AddToMapOp(columnName, map)); + } + } + + /** + * Builder to associate a single value with a collection at a given index at {@link ColumnName}. + * + * @author Mark Paluch + */ + public interface SetBuilder { + + /** + * Create a {@link SetValueBuilder} to set a value at a numeric {@code index}. Used for + * {@link com.datastax.driver.core.DataType.Name#LIST} type columns. + * + * @param index positional index. + * @return a {@link SetValueBuilder} to set a value at {@code index} + */ + SetValueBuilder atIndex(int index); + + /** + * Create a {@link SetValueBuilder} to set a value at {@code index}. Used for + * {@link com.datastax.driver.core.DataType.Name#MAP} type columns. + * + * @param key must not be {@literal null}. + * @return a {@link SetValueBuilder} to set a value at {@code index} + */ + SetValueBuilder atKey(Object key); + } + + /** + * Builder to associate a single value with a collection at a given index at {@link ColumnName}. + * + * @author Mark Paluch + */ + public interface SetValueBuilder { + + /** + * Associate the {@code value} with the collection at {@link ColumnName} with a previously specified index. + * + * @param value must not be {@literal null}. + * @return the {@link Update} object. + */ + Update to(Object value); + } + + /** + * Default {@link SetBuilder} implementation. + */ + private class DefaultSetBuilder implements SetBuilder { + + private final ColumnName columnName; + + DefaultSetBuilder(ColumnName columnName) { + this.columnName = columnName; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.SetBuilder#atIndex(int) + */ + @Override + public SetValueBuilder atIndex(int index) { + return value -> add(new SetAtIndexOp(columnName, index, value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.SetBuilder#atKey(java.lang.Object) + */ + @Override + public SetValueBuilder atKey(Object key) { + + Assert.notNull(key, "Key must not be null"); + + return value -> add(new SetAtKeyOp(columnName, key, value)); + } + } + + /** + * Abstract class for an update assignment related to a specific {@link ColumnName}. + */ + public abstract static class AssignmentOp { + + private final ColumnName columnName; + + protected AssignmentOp(ColumnName columnName) { + this.columnName = columnName; + } + + /** + * @return the {@link ColumnName}. + */ + public ColumnName getColumnName() { + return columnName; + } + } + + /** + * Add element(s) to collection operation. + */ + public static class AddToOp extends AssignmentOp { + + private final Iterable value; + private final Mode mode; + + @SuppressWarnings("unchecked") + public AddToOp(ColumnName columnName, Iterable value, Mode mode) { + + super(columnName); + + this.value = (Iterable) value; + this.mode = mode; + } + + public Iterable getValue() { + return value; + } + + public Mode getMode() { + return mode; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + if (mode == Mode.PREPEND) { + return String.format("%s = %s + %s", getColumnName(), serializeToCqlSafely(value), getColumnName()); + } + + return String.format("%s = %s + %s", getColumnName(), getColumnName(), serializeToCqlSafely(value)); + } + + public enum Mode { + PREPEND, APPEND, + } + } + + /** + * Add element(s) to Map operation. + */ + public static class AddToMapOp extends AssignmentOp { + + private final Map value; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public AddToMapOp(ColumnName columnName, Map value) { + + super(columnName); + this.value = (Map) value; + } + + public Map getValue() { + return value; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("%s = %s + %s", getColumnName(), getColumnName(), serializeToCqlSafely(value)); + } + } + + /** + * Set operation. + */ + public static class SetOp extends AssignmentOp { + + private final Object value; + + public SetOp(ColumnName columnName, Object value) { + + super(columnName); + this.value = value; + } + + public Object getValue() { + return value; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("%s = %s", getColumnName(), serializeToCqlSafely(value)); + } + } + + /** + * Set at index operation. + */ + public static class SetAtIndexOp extends SetOp { + + private final int index; + + public SetAtIndexOp(ColumnName columnName, int index, Object value) { + + super(columnName, value); + + Assert.notNull(value, "Value must not be null"); + + this.index = index; + } + + public int getIndex() { + return index; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.SetOp#toString() + */ + @Override + public String toString() { + return String.format("%s[%d] = %s", getColumnName(), index, serializeToCqlSafely(getValue())); + } + } + + /** + * Set at map key operation. + */ + public static class SetAtKeyOp extends SetOp { + + private final Object key; + private final Object value; + + public SetAtKeyOp(ColumnName columnName, Object key, Object value) { + + super(columnName, value); + + Assert.notNull(key, "Key must not be null"); + + this.key = key; + this.value = value; + } + + public Object getKey() { + return key; + } + + @Override + public Object getValue() { + return value; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.query.Update.SetOp#toString() + */ + @Override + public String toString() { + return String.format("%s[%s] = %s", getColumnName(), serializeToCqlSafely(key), serializeToCqlSafely(getValue())); + } + } + + /** + * Increment operation. + */ + public static class IncrOp extends AssignmentOp { + + private final Number value; + + public IncrOp(ColumnName columnName, Number value) { + + super(columnName); + this.value = value; + } + + public Number getValue() { + return value; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("%s = %s %s %d", getColumnName(), getColumnName(), value.doubleValue() > 0 ? "+" : "-", + Math.abs(value.intValue())); + } + } + + /** + * Remove operation. + */ + public static class RemoveOp extends AssignmentOp { + + private final Object value; + + public RemoveOp(ColumnName columnName, Object value) { + + super(columnName); + + Assert.notNull(value, "Value must not be null"); + + this.value = value; + } + + public Object getValue() { + return value; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("%s = %s - %s", getColumnName(), getColumnName(), serializeToCqlSafely(getValue())); + } + } +} 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 ed63f9365..16fe12b63 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 @@ -15,13 +15,20 @@ */ package org.springframework.data.cassandra.repository.query; +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; + import lombok.RequiredArgsConstructor; -import java.util.Map; - -import org.slf4j.Logger; -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.core.CassandraOperations; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.CollectionExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingConverter; @@ -38,6 +45,13 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Statement; + /** * Base class for {@link RepositoryQuery} implementations for Cassandra. * @@ -48,10 +62,10 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { protected static Logger log = LoggerFactory.getLogger(AbstractCassandraQuery.class); - private final CassandraOperations template; - private final CassandraQueryMethod queryMethod; + private final CassandraOperations operations; + private final EntityInstantiators instantiators; /** @@ -67,16 +81,45 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { Assert.notNull(operations, "CassandraOperations must not be null"); this.queryMethod = queryMethod; - this.template = operations; + this.operations = operations; this.instantiators = new EntityInstantiators(); } + /** + * @deprecated as of 1.5, {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type + * conversion. + */ + @Deprecated + public void setConversionService(ConversionService conversionService) { + throw new UnsupportedOperationException("setConversionService(ConversionService) is not supported anymore. " + + "Please use CassandraMappingContext instead"); + } + + /** + * @deprecated as of 1.5, {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type + * conversion. + */ + @Deprecated + public ConversionService getConversionService() { + return getOperations().getConverter().getConversionService(); + } + + /* (non-Javadoc) */ + protected EntityInstantiators getEntityInstantiators() { + return this.instantiators; + } + + /* (non-Javadoc) */ + protected CassandraOperations getOperations() { + return this.operations; + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() */ @Override public CassandraQueryMethod getQueryMethod() { - return queryMethod; + return this.queryMethod; } /* (non-Javadoc) @@ -85,57 +128,134 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { @Override public Object execute(Object[] parameters) { - CassandraParameterAccessor parameterAccessor = new ConvertingParameterAccessor(template.getConverter(), - new CassandraParametersParameterAccessor(queryMethod, parameters)); + CassandraParameterAccessor parameterAccessor = new ConvertingParameterAccessor(getOperations().getConverter(), + new CassandraParametersParameterAccessor(getQueryMethod(), parameters)); - ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + ResultProcessor resultProcessor = + getQueryMethod().getResultProcessor().withDynamicProjection(parameterAccessor); - String query = createQuery(parameterAccessor); + Statement statement = createQuery(parameterAccessor); - CassandraQueryExecution queryExecution = getExecution(query, parameterAccessor, - new ResultProcessingConverter(resultProcessor, template.getConverter().getMappingContext(), instantiators)); + CassandraQueryExecution queryExecution = getExecution( + new ResultProcessingConverter(resultProcessor, getOperations().getConverter().getMappingContext(), + getEntityInstantiators())); CassandraReturnedType returnedType = new CassandraReturnedType(resultProcessor.getReturnedType(), - template.getConverter().getCustomConversions()); + getOperations().getConverter().getCustomConversions()); Class resultType = (returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType()); - return queryExecution.execute(query, resultType); + return queryExecution.execute(statement, resultType); } /** * Returns the execution instance to use. * - * @param query must not be {@literal null}. - * @param accessor must not be {@literal null}. * @param resultProcessing must not be {@literal null}. @return */ - private CassandraQueryExecution getExecution(String query, CassandraParameterAccessor accessor, - Converter resultProcessing) { - - return new ResultProcessingExecution(getExecutionToWrap(accessor, resultProcessing), resultProcessing); + private CassandraQueryExecution getExecution(Converter resultProcessing) { + return new ResultProcessingExecution(getExecutionToWrap(resultProcessing), resultProcessing); } - private CassandraQueryExecution getExecutionToWrap(CassandraParameterAccessor accessor, - Converter resultProcessing) { + private CassandraQueryExecution getExecutionToWrap(Converter resultProcessing) { - if (queryMethod.isCollectionQuery()) { - return new CollectionExecution(template); - } else if (queryMethod.isResultSetQuery()) { - return new ResultSetQuery(template); - } else if (queryMethod.isStreamQuery()) { - return new StreamExecution(template, resultProcessing); + if (getQueryMethod().isCollectionQuery()) { + return new CollectionExecution(getOperations()); + } else if (getQueryMethod().isResultSetQuery()) { + return new ResultSetQuery(getOperations()); + } else if (getQueryMethod().isStreamQuery()) { + return new StreamExecution(getOperations(), resultProcessing); } else { - return new SingleEntityExecution(template); + return new SingleEntityExecution(getOperations()); } } /** +<<<<<<< 5cbbf085712acc72951fc9627d0f80c85dbb1505 * Creates a string query using the given {@link ParameterAccessor} +======= + * @param resultSet + * @param declaredReturnType + * @param returnedUnwrappedObjectType + * @return + * @deprecated as of 1.5, {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type + * conversion. + */ + @Deprecated + public Object getCollectionOfEntity(ResultSet resultSet, Class declaredReturnType, + Class returnedUnwrappedObjectType) { + + Collection results; + + if (ClassUtils.isAssignable(SortedSet.class, declaredReturnType)) { + results = new TreeSet<>(); + } else if (ClassUtils.isAssignable(Set.class, declaredReturnType)) { + results = new HashSet<>(); + } else { // List.class, Collection.class, or array + results = new ArrayList<>(); + } + + CassandraConverter converter = getOperations().getConverter(); + + for (Row row : resultSet) { + results.add(converter.read(returnedUnwrappedObjectType, row)); + } + + return results; + } + + /** + * @param resultSet + * @param type + * @return + * @deprecated as of 1.5, {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type + * conversion. + */ + @Deprecated + public Object getSingleEntity(ResultSet resultSet, Class type) { + + Object result = (resultSet.isExhausted() ? null : getOperations().getConverter().read(type, resultSet.one())); + + warnIfMoreResults(resultSet); + + return result; + } + + private void warnIfMoreResults(ResultSet resultSet) { + + if (log.isWarnEnabled() && !resultSet.isExhausted()) { + int count = 0; + + while (resultSet.one() != null) { + count++; + } + + log.warn("ignoring extra {} row{}", count, count == 1 ? "" : "s"); + } + } + + @Deprecated + protected void warnIfMoreResults(Iterator iterator) { + + if (log.isWarnEnabled() && iterator.hasNext()) { + int count = 0; + + while (iterator.hasNext()) { + count++; + iterator.next(); + } + + log.warn("ignoring extra {} row{}", count, count == 1 ? "" : "s"); + } + } + + /** + * Creates a {@link Statement} using the given {@link ParameterAccessor} +>>>>>>> DATACASS-343 - Introduce Query and Update objects. * * @param accessor must not be {@literal null}. */ - protected abstract String createQuery(CassandraParameterAccessor accessor); + protected abstract Statement createQuery(CassandraParameterAccessor accessor); @RequiredArgsConstructor private class CassandraReturnedType { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java index 3d9d0fb07..8f1587815 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractReactiveCassandraQuery.java @@ -32,6 +32,8 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.util.Assert; +import com.datastax.driver.core.Statement; + /** * Base class for reactive {@link RepositoryQuery} implementations for Cassandra. * @@ -40,11 +42,11 @@ import org.springframework.util.Assert; */ public abstract class AbstractReactiveCassandraQuery implements RepositoryQuery { - private final EntityInstantiators instantiators; + private final ReactiveCassandraQueryMethod method; private final ReactiveCassandraOperations operations; - private final ReactiveCassandraQueryMethod method; + private final EntityInstantiators instantiators; /** * Create a new {@link AbstractReactiveCassandraQuery} from the given {@link CassandraQueryMethod} and @@ -97,7 +99,7 @@ public abstract class AbstractReactiveCassandraQuery implements RepositoryQuery CassandraParameterAccessor convertingParameterAccessor = new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor); - String query = createQuery(convertingParameterAccessor); + Statement statement = createQuery(convertingParameterAccessor); ResultProcessor resultProcessor = method.getResultProcessor().withDynamicProjection(convertingParameterAccessor); @@ -109,7 +111,7 @@ public abstract class AbstractReactiveCassandraQuery implements RepositoryQuery Class resultType = (returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType()); - return queryExecution.execute(query, resultType); + return queryExecution.execute(statement, resultType); } /** @@ -117,7 +119,7 @@ public abstract class AbstractReactiveCassandraQuery implements RepositoryQuery * * @param accessor must not be {@literal null}. */ - protected abstract String createQuery(CassandraParameterAccessor accessor); + protected abstract Statement createQuery(CassandraParameterAccessor accessor); /** * Returns the execution instance to use. diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java index 4b8c3a99f..d2526fd27 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java @@ -19,19 +19,17 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.CriteriaDefinition; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; -import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator; import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -41,8 +39,6 @@ import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; import com.datastax.driver.core.querybuilder.Clause; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; /** * Custom query creator to create Cassandra criteria. @@ -51,19 +47,13 @@ import com.datastax.driver.core.querybuilder.Select; * @author Mark Paluch * @author John Blum */ -class CassandraQueryCreator extends AbstractQueryCreator { +class CassandraQueryCreator extends AbstractQueryCreator { private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); - private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{Punct}"); - private final CassandraMappingContext mappingContext; - private final CassandraPersistentEntity entity; - - private final CqlIdentifier tableName; - - private final WhereBuilder whereBuilder = new WhereBuilder(); + private final QueryBuilder queryBuilder = new QueryBuilder(); /** * Create a new {@link CassandraQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} and @@ -72,46 +62,42 @@ class CassandraQueryCreator extends AbstractQueryCreator { * @param tree must not be {@literal null}. * @param accessor must not be {@literal null}. * @param mappingContext must not be {@literal null}. - * @param entityMetadata must not be {@literal null}. */ public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, - CassandraMappingContext mappingContext, CassandraEntityMetadata entityMetadata) { + CassandraMappingContext mappingContext) { super(tree, accessor); Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); - Assert.notNull(entityMetadata, "CassandraEntityMetadata must not be null"); this.mappingContext = mappingContext; - this.entity = mappingContext.getRequiredPersistentEntity(entityMetadata.getJavaType()); - this.tableName = entityMetadata.getTableName(); } /* (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) { + protected CriteriaDefinition create(Part part, Iterator iterator) { PersistentPropertyPath path = mappingContext .getPersistentPropertyPath(part.getProperty()); CassandraPersistentProperty property = path.getLeafProperty(); - return from(part, property, (PotentiallyConvertingIterator) iterator); + return from(part, property, Criteria.where(path.toDotPath()), (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) { + protected CriteriaDefinition and(Part part, CriteriaDefinition base, Iterator iterator) { if (base == null) { - return whereBuilder.and(create(part, iterator)); + return queryBuilder.and(create(part, iterator)); } - whereBuilder.and(base); + queryBuilder.and(base); return create(part, iterator); } @@ -123,7 +109,7 @@ class CassandraQueryCreator extends AbstractQueryCreator { * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object) */ @Override - protected Clause or(Clause base, Clause criteria) { + protected CriteriaDefinition or(CriteriaDefinition base, CriteriaDefinition criteria) { throw new InvalidDataAccessApiUsageException("Cassandra does not support an OR operator"); } @@ -131,67 +117,64 @@ class CassandraQueryCreator extends AbstractQueryCreator { * @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) { + protected Query complete(CriteriaDefinition criteria, Sort sort) { if (criteria != null) { - whereBuilder.and(criteria); + queryBuilder.and(criteria); } - Select select = StatementBuilder.select(entity, tableName, whereBuilder, sort); + Query query = queryBuilder.create(sort); if (LOG.isDebugEnabled()) { - LOG.debug("Created query {}", select); + LOG.debug(String.format("Created query [%s]", query)); } - return select; + return query; } - private Clause from(Part part, CassandraPersistentProperty property, PotentiallyConvertingIterator parameters) { + private CriteriaDefinition from(Part part, CassandraPersistentProperty property, Criteria where, + PotentiallyConvertingIterator parameters) { Type type = part.getType(); switch (type) { case AFTER: case GREATER_THAN: - return QueryBuilder.gt(columnName(property), parameters.nextConverted(property)); + return where.gt(parameters.nextConverted(property)); case GREATER_THAN_EQUAL: - return QueryBuilder.gte(columnName(property), parameters.nextConverted(property)); + return where.gte(parameters.nextConverted(property)); case BEFORE: case LESS_THAN: - return QueryBuilder.lt(columnName(property), parameters.nextConverted(property)); + return where.lt(parameters.nextConverted(property)); case LESS_THAN_EQUAL: - return QueryBuilder.lte(columnName(property), parameters.nextConverted(property)); + return where.lte(parameters.nextConverted(property)); case IN: - return QueryBuilder.in(columnName(property), nextAsArray(property, parameters)); + return where.in(nextAsArray(property, parameters)); case LIKE: case STARTING_WITH: case ENDING_WITH: - return QueryBuilder.like(columnName(property), like(type, parameters.nextConverted(property))); + return where.like(like(type, parameters.nextConverted(property))); case CONTAINING: - return containing(property, parameters.nextConverted(property)); + return containing(where, property, parameters.nextConverted(property)); case TRUE: - return QueryBuilder.eq(columnName(property), true); + return where.is(true); case FALSE: - return QueryBuilder.eq(columnName(property), false); + return where.is(false); case SIMPLE_PROPERTY: - return QueryBuilder.eq(columnName(property), parameters.nextConverted(property)); + return where.is(parameters.nextConverted(property)); default: throw new InvalidDataAccessApiUsageException( String.format("Unsupported keyword [%s] in part [%s]", type, part)); } } - private static String columnName(CassandraPersistentProperty property) { - return property.getColumnName().toCql(); - } - - private Clause containing(CassandraPersistentProperty property, Object bindableValue) { + private CriteriaDefinition containing(Criteria where, CassandraPersistentProperty property, Object bindableValue) { if (property.isCollectionLike() || property.isMapLike()) { - return QueryBuilder.contains(columnName(property), bindableValue); + return where.contains(bindableValue); } - return QueryBuilder.like(columnName(property), like(Type.CONTAINING, bindableValue)); + return where.like(like(Type.CONTAINING, bindableValue)); } private Object like(Type type, Object value) { @@ -232,79 +215,20 @@ class CassandraQueryCreator extends AbstractQueryCreator { * * @author Mark Paluch */ - static class WhereBuilder { + static class QueryBuilder { - private List clauses = new ArrayList<>(); + private List criterias = new ArrayList<>(); - Clause and(Clause clause) { - clauses.add(clause); + CriteriaDefinition and(CriteriaDefinition clause) { + criterias.add(clause); return clause; } - Select.Where build(Select.Where where) { - for (Clause clause : clauses) { - where = where.and(clause); - } + Query create(Sort sort) { - 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}. - */ - static Select select(CassandraPersistentEntity entity, CqlIdentifier tableName, WhereBuilder whereBuilder, - Sort sort) { - - Select select = QueryBuilder.select().from(tableName.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; - } - - @SuppressWarnings("unchecked") - private static CassandraPersistentProperty getPersistentProperty(CassandraPersistentEntity entity, - String dotPath) { - - String[] segments = PUNCTUATION_PATTERN.split(dotPath); - - Optional property = Optional.empty(); - CassandraPersistentEntity currentEntity = entity; - - for (String segment : segments) { - - property = currentEntity.getPersistentProperty(segment); - currentEntity = property // - .filter(CassandraPersistentProperty::isCompositePrimaryKey) // - .map(CassandraPersistentProperty::getCompositePrimaryKeyEntity) // - .orElse((CassandraPersistentEntity) entity); - - } - - return property.orElseThrow(() -> new IllegalArgumentException( - String.format("Cannot resolve path [%s] to a property of [%s]", dotPath, entity.getName()))); + Query query = Query.query(criterias); + return query.sort(sort); } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java index 766b9f715..676e7d078 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.cassandra.repository.query; import lombok.NonNull; @@ -27,6 +26,8 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.Statement; + /** * Query executions for Cassandra. * @@ -35,7 +36,7 @@ import org.springframework.util.ClassUtils; */ interface CassandraQueryExecution { - Object execute(String query, Class type); + Object execute(Statement statement, Class type); /** * {@link CassandraQueryExecution} for a Stream. @@ -52,8 +53,8 @@ interface CassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.stream(query, type).map(resultProcessing::convert); + public Object execute(Statement statement, Class type) { + return operations.stream(statement, type).map(resultProcessing::convert); } } @@ -71,8 +72,8 @@ interface CassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.select(query, type); + public Object execute(Statement statement, Class type) { + return operations.select(statement, type); } } @@ -90,8 +91,8 @@ interface CassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.selectOne(query, type); + public Object execute(Statement statement, Class type) { + return operations.selectOne(statement, type); } } @@ -109,8 +110,8 @@ interface CassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.getCqlOperations().queryForResultSet(query); + public Object execute(Statement statement, Class type) { + return operations.getCqlOperations().queryForResultSet(statement); } } @@ -129,8 +130,8 @@ interface CassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return converter.convert(delegate.execute(query, type)); + public Object execute(Statement statement, Class type) { + return converter.convert(delegate.execute(statement, type)); } } 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 574ec08b3..c8b12568d 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 @@ -45,11 +45,11 @@ import com.datastax.driver.core.ResultSet; */ public class CassandraQueryMethod extends QueryMethod { - private CassandraEntityMetadata entityMetadata; + private final Method method; private final CassandraMappingContext mappingContext; - private final Method method; + private CassandraEntityMetadata entityMetadata; /** * Create a new {@link CassandraQueryMethod} from the given {@link Method}. @@ -166,6 +166,11 @@ public class CassandraQueryMethod extends QueryMethod { return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); } + @Override + protected Class getDomainClass() { + return super.getDomainClass(); + } + /** * @return the return type for this {@link QueryMethod}. */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java index 92b1b650c..49658d290 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java @@ -37,6 +37,7 @@ import org.springframework.util.CollectionUtils; class ExpressionEvaluatingParameterBinder { private final SpelExpressionParser expressionParser; + private final EvaluationContextProvider evaluationContextProvider; /** 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 9d3d9b189..4eba9d2e6 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 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -15,14 +15,20 @@ */ package org.springframework.data.cassandra.repository.query; +import org.springframework.data.cassandra.convert.UpdateMapper; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.StatementFactory; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; +import com.datastax.driver.core.Statement; + /** * {@link RepositoryQuery} implementation for Cassandra. * @@ -31,9 +37,11 @@ import org.springframework.data.repository.query.parser.PartTree; */ public class PartTreeCassandraQuery extends AbstractCassandraQuery { + private final PartTree tree; + private final CassandraMappingContext mappingContext; - private final PartTree tree; + private final StatementFactory statementFactory; /** * Create a new {@link PartTreeCassandraQuery} from the given {@link QueryMethod} and {@link CassandraTemplate}. @@ -45,12 +53,30 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { super(queryMethod, operations); - try { - this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); - this.mappingContext = operations.getConverter().getMappingContext(); - } catch (Exception e) { - throw QueryCreationException.create(queryMethod, e); - } + this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); + this.mappingContext = operations.getConverter().getMappingContext(); + this.statementFactory = new StatementFactory(new UpdateMapper(operations.getConverter())); + } + + /** + * Returns the {@link CassandraMappingContext} used by this query to access mapping meta-data used to + * store (map) objects to Cassandra tables. + * + * @return the {@link CassandraMappingContext} used by this query. + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext + */ + protected CassandraMappingContext getMappingContext() { + return this.mappingContext; + } + + /** + * Returns the {@link StatementFactory} used by this query to construct and run Cassandra CQL statements. + * + * @return the {@link StatementFactory} used by this query to construct and run Cassandra CQL statements. + * @see org.springframework.data.cassandra.core.StatementFactory + */ + protected StatementFactory getStatementFactory() { + return this.statementFactory; } /** @@ -58,8 +84,8 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { * * @return the tree */ - public PartTree getTree() { - return tree; + protected PartTree getTree() { + return this.tree; } /* @@ -67,11 +93,24 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean) */ @Override - protected String createQuery(CassandraParameterAccessor parameterAccessor) { + protected Statement createQuery(CassandraParameterAccessor parameterAccessor) { - CassandraQueryCreator queryCreator = new CassandraQueryCreator(tree, parameterAccessor, mappingContext, - getQueryMethod().getEntityInformation()); + CassandraQueryCreator queryCreator = + new CassandraQueryCreator(getTree(), parameterAccessor, getMappingContext()); - return queryCreator.createQuery().toString(); + Query query = queryCreator.createQuery(); + + try { + if (getTree().isLimiting()) { + query.limit(getTree().getMaxResults()); + } + + CassandraPersistentEntity persistentEntity = + getMappingContext().getRequiredPersistentEntity(getQueryMethod().getDomainClass()); + + return getStatementFactory().select(query, persistentEntity); + } catch (RuntimeException e) { + throw QueryCreationException.create(getQueryMethod(), e); + } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraParameterAccessor.java index d1f133b48..536ebf41d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -35,6 +35,7 @@ import org.springframework.data.repository.util.ReactiveWrappers; class ReactiveCassandraParameterAccessor extends CassandraParametersParameterAccessor { private final Object[] values; + private final List> subscriptions; public ReactiveCassandraParameterAccessor(CassandraQueryMethod method, Object[] values) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java index 3b0213d07..a1a98cd7e 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveCassandraQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.cassandra.repository.query; import lombok.NonNull; @@ -27,6 +26,8 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.Statement; + /** * Reactive query executions for Cassandra. * @@ -35,7 +36,7 @@ import org.springframework.util.ClassUtils; */ interface ReactiveCassandraQueryExecution { - Object execute(String query, Class type); + Object execute(Statement statement, Class type); /** * {@link ReactiveCassandraQueryExecution} for collection returning queries. @@ -52,8 +53,8 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.select(query, type); + public Object execute(Statement statement, Class type) { + return operations.select(statement, type); } } @@ -72,8 +73,8 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return operations.selectOne(query, type); + public Object execute(Statement statement, Class type) { + return operations.selectOne(statement, type); } } @@ -94,8 +95,8 @@ interface ReactiveCassandraQueryExecution { * @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class) */ @Override - public Object execute(String query, Class type) { - return converter.convert(delegate.execute(query, type)); + public Object execute(Statement statement, Class type) { + return converter.convert(delegate.execute(statement, type)); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java index 031d10364..d0b5ccaa0 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -15,12 +15,18 @@ */ package org.springframework.data.cassandra.repository.query; +import org.springframework.data.cassandra.convert.UpdateMapper; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.core.StatementFactory; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; +import com.datastax.driver.core.Statement; + /** * Reactive PartTree {@link RepositoryQuery} implementation for Cassandra. * @@ -29,9 +35,11 @@ import org.springframework.data.repository.query.parser.PartTree; */ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQuery { + private final PartTree tree; + private final CassandraMappingContext mappingContext; - private final PartTree tree; + private final StatementFactory statementFactory; /** * Create a new {@link ReactivePartTreeCassandraQuery} from the given {@link ReactiveCassandraQueryMethod} and @@ -45,12 +53,30 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue super(queryMethod, operations); - try { - this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); - this.mappingContext = operations.getConverter().getMappingContext(); - } catch (Exception e) { - throw QueryCreationException.create(queryMethod, e); - } + this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); + this.mappingContext = operations.getConverter().getMappingContext(); + this.statementFactory = new StatementFactory(new UpdateMapper(operations.getConverter())); + } + + /** + * Returns the {@link CassandraMappingContext} used by this query to access mapping meta-data used to + * store (map) objects to Cassandra tables. + * + * @return the {@link CassandraMappingContext} used by this query. + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext + */ + protected CassandraMappingContext getMappingContext() { + return this.mappingContext; + } + + /** + * Returns the {@link StatementFactory} used by this query to construct and run Cassandra CQL statements. + * + * @return the {@link StatementFactory} used by this query to construct and run Cassandra CQL statements. + * @see org.springframework.data.cassandra.core.StatementFactory + */ + protected StatementFactory getStatementFactory() { + return this.statementFactory; } /** @@ -58,8 +84,8 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue * * @return the tree */ - public PartTree getTree() { - return tree; + protected PartTree getTree() { + return this.tree; } /* @@ -67,11 +93,24 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean) */ @Override - protected String createQuery(CassandraParameterAccessor parameterAccessor) { + protected Statement createQuery(CassandraParameterAccessor parameterAccessor) { - CassandraQueryCreator queryCreator = new CassandraQueryCreator(tree, parameterAccessor, mappingContext, - getQueryMethod().getEntityInformation()); + CassandraQueryCreator queryCreator = + new CassandraQueryCreator(getTree(), parameterAccessor, getMappingContext()); - return queryCreator.createQuery().toString(); + Query query = queryCreator.createQuery(); + + try { + if (getTree().isLimiting()) { + query.limit(getTree().getMaxResults()); + } + + CassandraPersistentEntity persistentEntity = + getMappingContext().getRequiredPersistentEntity(getQueryMethod().getDomainClass()); + + return getStatementFactory().select(query, persistentEntity); + } catch (RuntimeException e) { + throw QueryCreationException.create(getQueryMethod(), e); + } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java index 5cb8b8818..f30ee6cf7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -15,22 +15,18 @@ */ package org.springframework.data.cassandra.repository.query; -import reactor.core.publisher.Flux; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cassandra.core.ReactiveSessionCallback; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.SimpleStatement; /** - * String-based {@link AbstractCassandraQuery} implementation. + * String-based {@link AbstractReactiveCassandraQuery} implementation. *

* A {@link ReactiveStringBasedCassandraQuery} expects a query method to be annotated with * {@link org.springframework.data.cassandra.repository.Query} with a CQL query. String-based queries support named, @@ -81,24 +77,18 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra Assert.hasText(query, "Query must not be empty"); - // this blocking operation is to retrieve the underlying Cluster and does not include any I/O here. - Cluster cluster = operations.getReactiveCqlOperations() - .execute((ReactiveSessionCallback) session -> Flux.just(session.getCluster())).blockFirst(); - - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - this.stringBasedQuery = new StringBasedQuery(query, - new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider), codecRegistry); + new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor) */ @Override - public String createQuery(CassandraParameterAccessor parameterAccessor) { + public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { try { - String boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod()); + SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod()); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Created query [%s].", boundQuery)); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java index ac0b43878..43051c000 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -22,9 +22,7 @@ import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.expression.spel.standard.SpelExpressionParser; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.Session; +import com.datastax.driver.core.SimpleStatement; /** * String-based {@link AbstractCassandraQuery} implementation. @@ -74,22 +72,19 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) { super(queryMethod, operations); - - Cluster cluster = operations.getCqlOperations().execute(Session::getCluster); - - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - this.stringBasedQuery = new StringBasedQuery(query, - new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider), codecRegistry); + new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor) */ @Override - public String createQuery(CassandraParameterAccessor parameterAccessor) { + public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) { + try { - String boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod()); + + SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod()); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Created query [%s].", boundQuery)); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java index 95c94d500..635799236 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedQuery.java @@ -16,10 +16,7 @@ package org.springframework.data.cassandra.repository.query; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,8 +27,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.TypeCodec; -import com.datastax.driver.core.querybuilder.BindMarker; +import com.datastax.driver.core.SimpleStatement; /** * String-based Query abstracting a CQL query with parameter bindings. @@ -41,11 +37,12 @@ import com.datastax.driver.core.querybuilder.BindMarker; */ class StringBasedQuery { - private final CodecRegistry codecRegistry; - private final ExpressionEvaluatingParameterBinder parameterBinder; - private final List queryParameterBindings = new ArrayList<>(); private final String query; + private final ExpressionEvaluatingParameterBinder parameterBinder; + + private final List queryParameterBindings = new ArrayList<>(); + /** * Create a new {@link StringBasedQuery} given {@code query}, {@link ExpressionEvaluatingParameterBinder} and * {@link CodecRegistry}. @@ -54,14 +51,11 @@ class StringBasedQuery { * @param parameterBinder must not be {@literal null}. * @param codecRegistry must not be {@literal null}. */ - public StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder, - CodecRegistry codecRegistry) { + public StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder) { Assert.hasText(query, "Query must not be empty"); Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null"); - Assert.notNull(codecRegistry, "CodecRegistry must not be null"); - this.codecRegistry = codecRegistry; this.parameterBinder = parameterBinder; this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, @@ -76,7 +70,7 @@ class StringBasedQuery { * @param queryMethod must not be {@literal null}. * @return the bound String query containing formatted parameters. */ - public String bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) { + public SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) { Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null"); Assert.notNull(queryMethod, "CassandraQueryMethod must not be null"); @@ -84,7 +78,7 @@ class StringBasedQuery { List arguments = parameterBinder.bind(parameterAccessor, new BindingContext(queryMethod, queryParameterBindings)); - return ParameterBinder.INSTANCE.bind(query, codecRegistry, arguments); + return ParameterBinder.INSTANCE.bind(query, arguments); } /** @@ -99,10 +93,10 @@ class StringBasedQuery { private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER)); - public String bind(String input, CodecRegistry codecRegistry, List parameters) { + public SimpleStatement bind(String input, List parameters) { if (parameters.isEmpty()) { - return input; + return new SimpleStatement(input); } StringBuilder result = new StringBuilder(); @@ -110,7 +104,6 @@ class StringBasedQuery { int startIndex = 0; int currentPosition = 0; int parameterIndex = 0; - Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input); while (currentPosition < input.length()) { @@ -121,141 +114,16 @@ class StringBasedQuery { int exprStart = matcher.start(); - result.append(input.subSequence(startIndex, exprStart)); - result = appendValue(parameters.get(parameterIndex++), codecRegistry, result); + result.append(input.subSequence(startIndex, exprStart)).append("?"); + parameterIndex++; currentPosition = matcher.end(); startIndex = currentPosition; } - return result.append(input.subSequence(currentPosition, input.length())).toString(); - } + String bindableStatement = result.append(input.subSequence(currentPosition, input.length())).toString(); - static StringBuilder appendValue(Object value, CodecRegistry codecRegistry, StringBuilder builder) { - - if (value == null) { - builder.append("null"); - } else if (value instanceof BindMarker) { - builder.append(value); - } else if (value instanceof List && isSerializable(value)) { - // bind variables are not supported inside collection literals - appendList((List) value, codecRegistry, builder); - } else if (value instanceof Set && isSerializable(value)) { - // bind variables are not supported inside collection literals - appendSet((Set) value, codecRegistry, builder); - } else if (value instanceof Map && isSerializable(value)) { - // bind variables are not supported inside collection literals - appendMap((Map) value, codecRegistry, builder); - } else if (isSerializable(value)) { - TypeCodec codec = codecRegistry.codecFor(value); - builder.append(codec.format(value)); - } else { - throw new IllegalArgumentException(String.format("Argument value [%s] is not serializable", value.toString())); - } - - return builder; - } - - private static StringBuilder appendList(List list, CodecRegistry codecRegistry, StringBuilder builder) { - - for (int index = 0, size = list.size(); index < size; index++) { - builder.append(index > 0 ? "," : ""); - appendValue(list.get(index), codecRegistry, builder); - } - - return builder; - } - - private static StringBuilder appendSet(Set set, CodecRegistry codecRegistry, StringBuilder builder) { - - boolean first = true; - - for (Object element : set) { - builder.append(first ? "" : ","); - appendValue(element, codecRegistry, builder); - first = false; - } - - return builder; - } - - private static StringBuilder appendMap(Map map, CodecRegistry codecRegistry, StringBuilder builder) { - - builder.append('{'); - - boolean first = true; - - for (Map.Entry entry : map.entrySet()) { - builder.append(first ? "" : ","); - appendValue(entry.getKey(), codecRegistry, builder); - builder.append(':'); - appendValue(entry.getValue(), codecRegistry, builder); - first = false; - } - - builder.append('}'); - - return builder; - } - - /** - * Return true if the given value is likely to find a suitable codec to be serialized as a query parameter. If the - * value is not serializable, it must be included in the query string. Non serializable values include special - * values such as function calls, column names and bind markers, and collections thereof. We also don't serialize - * fixed size number types. The reason is that if we do it, we will force a particular size (4 bytes for ints, ...) - * and for the query builder, we don't want users to have to bother with that. - * - * @param value the value to inspect. - * @return true if the value is serializable, false otherwise. - */ - static boolean isSerializable(Object value) { - - if (containsSpecialValue(value)) { - return false; - } - - if (value instanceof Collection) { - for (Object element : (Collection) value) { - if (!isSerializable(element)) { - return false; - } - } - } - - if (value instanceof Map) { - for (Map.Entry entry : ((Map) value).entrySet()) { - if (!isSerializable(entry.getKey()) || !isSerializable(entry.getValue())) { - return false; - } - } - } - - return true; - } - - static boolean containsSpecialValue(Object value) { - - if (value instanceof BindMarker) { - return true; - } - - if (value instanceof Collection) { - for (Object element : (Collection) value) { - if (containsSpecialValue(element)) { - return true; - } - } - } - - if (value instanceof Map) { - for (Map.Entry entry : ((Map) value).entrySet()) { - if (containsSpecialValue(entry.getKey()) || containsSpecialValue(entry.getValue())) { - return true; - } - } - } - - return false; + return new SimpleStatement(bindableStatement, parameters.subList(0, parameterIndex).toArray()); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java index 5fb047e80..bc4421e47 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java @@ -30,8 +30,7 @@ import org.springframework.util.Assert; /** * {@link CassandraEntityInformation} implementation using a {@link CassandraPersistentEntity} instance to lookup the - * necessary information. Can be configured with a custom collection to be returned which will trump the one returned by - * the {@link CassandraPersistentEntity} if given. + * necessary information. * * @author Alex Shvid * @author Matthew T. Adams diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CurrencyConverter.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CurrencyConverter.java new file mode 100644 index 000000000..7c79cca39 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CurrencyConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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.convert; + +import java.util.Currency; +import java.util.Locale; + +import org.springframework.core.convert.converter.Converter; + +/** + * See also DATACASS-343. + * + * @author Mark Paluch + */ +enum CurrencyConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Currency source) { + return source.getDisplayName(Locale.ENGLISH); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java index 378ee9ded..09ebaf1f6 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java @@ -794,7 +794,7 @@ public class MappingCassandraConverterUnitTests { mappingCassandraConverter.write(entity, delete.where(), mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); - assertThat(getWherePredicates(delete)).containsEntry("firstname", "Walter"); + assertThat(getWherePredicates(delete)).containsEntry("first_name", "Walter"); assertThat(getWherePredicates(delete)).containsEntry("lastname", "White"); } @@ -819,7 +819,7 @@ public class MappingCassandraConverterUnitTests { mappingCassandraConverter.write(key, delete.where(), mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); - assertThat(getWherePredicates(delete)).containsEntry("firstname", "Walter"); + assertThat(getWherePredicates(delete)).containsEntry("first_name", "Walter"); assertThat(getWherePredicates(delete)).containsEntry("lastname", "White"); } @@ -831,7 +831,7 @@ public class MappingCassandraConverterUnitTests { mappingCassandraConverter.write(id("firstname", "Walter").with("lastname", "White"), delete.where(), mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); - assertThat(getWherePredicates(delete)).containsEntry("firstname", "Walter"); + assertThat(getWherePredicates(delete)).containsEntry("first_name", "Walter"); assertThat(getWherePredicates(delete)).containsEntry("lastname", "White"); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/QueryMapperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/QueryMapperUnitTests.java new file mode 100644 index 000000000..83b6b9fcd --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/QueryMapperUnitTests.java @@ -0,0 +1,321 @@ +/* + * Copyright 2017 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.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; + +import java.util.Collection; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.query.ColumnName; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Columns.Selector; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.CriteriaDefinition; +import org.springframework.data.cassandra.core.query.CriteriaDefinition.Operators; +import org.springframework.data.cassandra.core.query.Filter; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.domain.TypeWithKeyClass; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.cassandra.mapping.UserDefinedType; +import org.springframework.data.cassandra.mapping.UserTypeResolver; +import org.springframework.data.cassandra.support.UserTypeBuilder; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; + +/** + * Unit tests for {@link QueryMapper}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class QueryMapperUnitTests { + + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + CassandraPersistentEntity persistentEntity; + MappingCassandraConverter cassandraConverter; + QueryMapper queryMapper; + + @Mock UserTypeResolver userTypeResolver; + + UserType userType = UserTypeBuilder.forName("address").withField("street", DataType.varchar()).build(); + + @Before + public void before() throws Exception { + + CustomConversions customConversions = new CustomConversions(Collections.singletonList(CurrencyConverter.INSTANCE)); + + mappingContext.setCustomConversions(customConversions); + mappingContext.setUserTypeResolver(userTypeResolver); + + cassandraConverter = new MappingCassandraConverter(mappingContext); + cassandraConverter.setCustomConversions(customConversions); + cassandraConverter.afterPropertiesSet(); + + queryMapper = new QueryMapper(cassandraConverter); + + when(userTypeResolver.resolveType(any(CqlIdentifier.class))).thenReturn(userType); + + persistentEntity = mappingContext.getRequiredPersistentEntity(Person.class); + } + + @Test // DATACASS-343 + public void shouldMapSimpleQuery() { + + Query query = Query.query(Criteria.where("foo_name").is("bar")); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.EQ); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isEqualTo("bar"); + } + + @Test // DATACASS-343 + public void shouldMapEnumToString() { + + Query query = Query.query(Criteria.where("foo_name").is(State.Active)); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(String.class).isEqualTo("Active"); + } + + @Test // DATACASS-343 + public void shouldMapEnumToNumber() { + + Query query = Query.query(Criteria.where("number").is(State.Inactive)); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(Integer.class).isEqualTo(1); + } + + @Test // DATACASS-343 + public void shouldMapEnumToNumberIn() { + + Query query = Query.query(Criteria.where("number").in(State.Inactive)); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(Collection.class) + .isEqualTo(Collections.singletonList(1)); + } + + @Test // DATACASS-343 + public void shouldMapApplyingCustomConversion() { + + Query query = Query.query(Criteria.where("foo_name").is(Currency.getInstance("EUR"))); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.EQ); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isEqualTo("Euro"); + } + + @Test // DATACASS-343 + public void shouldMapApplyingCustomConversionInCollection() { + + Query query = Query.query(Criteria.where("foo_name").in(Currency.getInstance("EUR"))); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.IN); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isEqualTo(Collections.singletonList("Euro")); + } + + @Test // DATACASS-343 + public void shouldMapApplyingUdtValueConversion() { + + Query query = Query.query(Criteria.where("address").is(new Address("21 Jump-Street"))); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.EQ); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(UDTValue.class); + assertThat(mappedCriteriaDefinition.getPredicate().getValue().toString()).isEqualTo("{street:'21 Jump-Street'}"); + } + + @Test // DATACASS-343 + public void shouldMapApplyingUdtValueCollectionConversion() { + + Query query = Query.query(Criteria.where("address").in(new Address("21 Jump-Street"))); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.IN); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(Collection.class); + assertThat(mappedCriteriaDefinition.getPredicate().getValue().toString()).isEqualTo("[{street:'21 Jump-Street'}]"); + } + + @Test // DATACASS-343 + public void shouldMapCollectionApplyingUdtValueCollectionConversion() { + + Query query = Query.query(Criteria.where("addresses").in(new Address("21 Jump-Street"))); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getPredicate().getOperator()).isEqualTo(Operators.IN); + assertThat(mappedCriteriaDefinition.getPredicate().getValue()).isInstanceOf(Collection.class); + assertThat(mappedCriteriaDefinition.getPredicate().getValue().toString()).isEqualTo("[{street:'21 Jump-Street'}]"); + } + + @Test // DATACASS-343 + public void shouldMapPropertyToColumnName() { + + Query query = Query.query(Criteria.where("firstName").is("bar")); + + Filter mappedObject = queryMapper.getMappedObject(query, persistentEntity); + + CriteriaDefinition mappedCriteriaDefinition = mappedObject.iterator().next(); + + assertThat(mappedCriteriaDefinition.getColumnName()).isEqualTo(ColumnName.from(CqlIdentifier.cqlId("first_name"))); + assertThat(mappedCriteriaDefinition.getColumnName().toString()).isEqualTo("first_name"); + } + + @Test // DATACASS-343 + public void shouldCreateSelectExpression() { + + List selectors = queryMapper.getMappedSelectors(Columns.empty(), persistentEntity); + + assertThat(selectors).isEmpty(); + } + + @Test // DATACASS-343 + public void shouldCreateSelectExpressionWithTTL() { + + List selectors = queryMapper + .getMappedSelectors(Columns.from("number", "foo").ttl("firstName"), + mappingContext.getRequiredPersistentEntity(Person.class)) + .stream().map(Selector::toString).collect(Collectors.toList()); + + assertThat(selectors).contains("number").contains("foo").contains("TTL(first_name)"); + } + + @Test // DATACASS-343 + public void shouldIncludeColumnsSelectExpressionWithTTL() { + + List selectors = queryMapper.getMappedColumnNames(Columns.from("number", "foo").ttl("firstName"), + persistentEntity); + + assertThat(selectors).contains("number").contains("foo").hasSize(2); + } + + @Test // DATACASS-343 + public void shouldMapQueryWithCompositePrimaryKeyClass() { + + Filter filter = Filter.from(Criteria.where("key.firstname").is("foo")); + + Filter mappedObject = queryMapper.getMappedObject(filter, + mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); + + assertThat(mappedObject).contains(Criteria.where("first_name").is("foo")); + } + + @Test // DATACASS-343 + public void shouldMapSortWithCompositePrimaryKeyClass() { + + Sort sort = Sort.by("key.firstname"); + + Sort mappedObject = queryMapper.getMappedSort(sort, + mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); + + assertThat(mappedObject).contains(new Order(Direction.ASC, "first_name")); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-343 + public void shouldFailMappingSortByCompositePrimaryKeyClass() { + + Sort sort = Sort.by("key"); + + queryMapper.getMappedSort(sort, mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); + } + + @Test // DATACASS-343 + public void shouldMapColumnWithCompositePrimaryKeyClass() { + + Columns columnNames = Columns.from("key.firstname"); + + List mappedObject = queryMapper.getMappedColumnNames(columnNames, + mappingContext.getRequiredPersistentEntity(TypeWithKeyClass.class)); + + assertThat(mappedObject).contains("first_name"); + } + + static class Person { + + @Id String id; + + Address address; + List
addresses; + Currency currency; + State state; + + Integer number; + + @Column("first_name") String firstName; + } + + @UserDefinedType + @AllArgsConstructor + static class Address { + + String street; + } + + enum State { + Active, Inactive; + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/UpdateMapperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/UpdateMapperUnitTests.java new file mode 100644 index 000000000..08f1295c4 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/UpdateMapperUnitTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2017 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.convert; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.cassandra.mapping.UserTypeResolver; + +/** + * Unit tests for {@link UpdateMapper}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class UpdateMapperUnitTests { + + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + CassandraPersistentEntity persistentEntity; + MappingCassandraConverter cassandraConverter; + UpdateMapper updateMapper; + + @Mock UserTypeResolver userTypeResolver; + + Currency currency = Currency.getInstance("EUR"); + + @Before + public void before() throws Exception { + + CustomConversions customConversions = new CustomConversions(Collections.singletonList(CurrencyConverter.INSTANCE)); + + mappingContext.setCustomConversions(customConversions); + mappingContext.setUserTypeResolver(userTypeResolver); + + cassandraConverter = new MappingCassandraConverter(mappingContext); + cassandraConverter.setCustomConversions(customConversions); + cassandraConverter.afterPropertiesSet(); + + updateMapper = new UpdateMapper(cassandraConverter); + + persistentEntity = mappingContext.getRequiredPersistentEntity(Person.class); + } + + @Test // DATACASS-343 + public void shouldCreateSimpleUpdate() { + + Update update = updateMapper.getMappedObject(Update.empty().set("firstName", "foo"), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("first_name = 'foo'"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtIndexUpdate() { + + Update update = updateMapper.getMappedObject(Update.empty().set("list").atIndex(10).to(currency), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("list[10] = 'Euro'"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtKeyUpdate() { + + Update update = updateMapper.getMappedObject(Update.empty().set("map").atKey("baz").to(currency), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("map['baz'] = 'Euro'"); + } + + @Test // DATACASS-343 + public void shouldAddToMap() { + + Update update = updateMapper.getMappedObject(Update.empty().addTo("map").entry("foo", currency), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("map = map + {'foo':'Euro'}"); + } + + @Test // DATACASS-343 + public void shouldPrependAllToList() { + + Update update = updateMapper.getMappedObject(Update.empty().addTo("list").prependAll("foo", currency), + persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("list = ['foo','Euro'] + list"); + } + + @Test // DATACASS-343 + public void shouldAppendAllToList() { + + Update update = updateMapper.getMappedObject(Update.empty().addTo("list").appendAll("foo", currency), + persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("list = list + ['foo','Euro']"); + } + + @Test // DATACASS-343 + public void shouldRemoveFromList() { + + Update update = updateMapper.getMappedObject(Update.empty().remove("list", currency), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("list = list - ['Euro']"); + } + + @Test // DATACASS-343 + public void shouldClearList() { + + Update update = updateMapper.getMappedObject(Update.empty().clear("list"), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("list = []"); + } + + @Test // DATACASS-343 + public void shouldClearSet() { + + Update update = updateMapper.getMappedObject(Update.empty().clear("set"), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("set_col = {}"); + } + + @Test // DATACASS-343 + public void shouldCreateIncrementUpdate() { + + Update update = updateMapper.getMappedObject(Update.empty().increment("number"), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("number = number + 1"); + } + + @Test // DATACASS-343 + public void shouldCreateDecrementUpdate() { + + Update update = updateMapper.getMappedObject(Update.empty().decrement("number"), persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("number = number - 1"); + } + + static class Person { + + @Id String id; + + List list; + @Column("set_col") Set set; + Map map; + Currency currency; + + Integer number; + + @Column("first_name") String firstName; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java index aefb8b241..b750bfb5f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java @@ -24,10 +24,18 @@ import org.junit.Test; import org.springframework.cassandra.core.AsyncCqlTemplate; import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.domain.UserToken; import org.springframework.data.cassandra.test.integration.support.SchemaTestUtils; +import org.springframework.data.domain.Sort; import org.springframework.util.concurrent.ListenableFuture; +import com.datastax.driver.core.utils.UUIDs; + /** * Integration tests for {@link AsyncCassandraTemplate}. * @@ -45,7 +53,45 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea template = new AsyncCassandraTemplate(new AsyncCqlTemplate(session), converter); SchemaTestUtils.potentiallyCreateTableFor(Person.class, cassandraTemplate); + SchemaTestUtils.potentiallyCreateTableFor(UserToken.class, cassandraTemplate); SchemaTestUtils.truncate(Person.class, cassandraTemplate); + SchemaTestUtils.truncate(UserToken.class, cassandraTemplate); + } + + @Test // DATACASS-343 + public void shouldSelectByQueryWithSorting() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + UserToken token2 = new UserToken(); + token2.setUserId(token1.getUserId()); + token2.setToken(UUIDs.endOf(System.currentTimeMillis() + 100)); + token2.setUserComment("bar"); + + getUninterruptibly(template.insert(token1)); + getUninterruptibly(template.insert(token2)); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())).sort(Sort.by("token")); + + assertThat(getUninterruptibly(template.select(query, UserToken.class))).containsSequence(token1, token2); + } + + @Test // DATACASS-343 + public void shouldSelectOneByQuery() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + getUninterruptibly(template.insert(token1)); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())); + + assertThat(getUninterruptibly(template.selectOne(query, UserToken.class))).isEqualTo(token1); } @Test // DATACASS-292 @@ -85,6 +131,48 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea assertThat(getUninterruptibly(template.selectOneById(person.getId(), Person.class))).isEqualTo(person); } + @Test // DATACASS-343 + public void updateShouldUpdateEntityByQuery() throws Exception { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person).get(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + boolean result = getUninterruptibly( + template.update(query, Update.empty().set("firstname", "Walter Hartwell"), Person.class)); + assertThat(result).isTrue(); + + assertThat(getUninterruptibly(template.selectOneById(person.getId(), Person.class)).getFirstname()) + .isEqualTo("Walter Hartwell"); + } + + @Test // DATACASS-343 + public void deleteByQueryShouldRemoveEntity() throws Exception { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person).get(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + assertThat(getUninterruptibly(template.delete(query, Person.class))).isTrue(); + + assertThat(getUninterruptibly(template.selectOneById(person.getId(), Person.class))).isNull(); + } + + @Test // DATACASS-343 + public void deleteColumnsByQueryShouldRemoveColumn() throws Exception { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person).get(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")).columns(Columns.from("lastname")); + + assertThat(getUninterruptibly(template.delete(query, Person.class))).isTrue(); + + Person loaded = getUninterruptibly(template.selectOneById(person.getId(), Person.class)); + assertThat(loaded.getFirstname()).isEqualTo("Walter"); + assertThat(loaded.getLastname()).isNull(); + } + @Test // DATACASS-292 public void deleteShouldRemoveEntity() throws Exception { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java index 21e8bf2cf..5ff7ff9e1 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java @@ -16,9 +16,11 @@ package org.springframework.data.cassandra.core; import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,12 +28,19 @@ import org.junit.Before; import org.junit.Test; import org.springframework.cassandra.core.CqlTemplate; import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest; +import org.springframework.cassandra.test.integration.support.CassandraVersion; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.Person; import org.springframework.data.cassandra.domain.UserToken; import org.springframework.data.cassandra.repository.support.BasicMapId; import org.springframework.data.cassandra.test.integration.simpletons.BookReference; import org.springframework.data.cassandra.test.integration.support.SchemaTestUtils; +import org.springframework.data.domain.Sort; +import org.springframework.data.util.Version; import com.datastax.driver.core.utils.UUIDs; @@ -42,7 +51,11 @@ import com.datastax.driver.core.utils.UUIDs; */ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest { - private CassandraTemplate template; + final static Version CASSANDRA_3 = Version.parse("3.0"); + + Version cassandraVersion; + + CassandraTemplate template; @Before public void setUp() { @@ -50,6 +63,8 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI MappingCassandraConverter converter = new MappingCassandraConverter(); converter.afterPropertiesSet(); + cassandraVersion = CassandraVersion.get(session); + template = new CassandraTemplate(new CqlTemplate(session), converter); SchemaTestUtils.potentiallyCreateTableFor(Person.class, template); @@ -60,6 +75,64 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI SchemaTestUtils.truncate(BookReference.class, template); } + @Test // DATACASS-343 + public void shouldSelectByQueryWithAllowFiltering() { + + assumeTrue(cassandraVersion.isGreaterThanOrEqualTo(CASSANDRA_3)); + + UserToken userToken = new UserToken(); + userToken.setUserId(UUIDs.endOf(System.currentTimeMillis())); + userToken.setToken(UUIDs.startOf(System.currentTimeMillis())); + userToken.setUserComment("cook"); + + template.insert(userToken); + + Query query = Query.query(Criteria.where("userId").is(userToken.getUserId())) + .and(Criteria.where("userComment").is("cook")).withAllowFiltering(); + UserToken loaded = template.selectOne(query, UserToken.class); + + assertThat(loaded).isNotNull(); + assertThat(loaded.getUserComment()).isEqualTo("cook"); + } + + @Test // DATACASS-343 + public void shouldSelectByQueryWithSorting() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + UserToken token2 = new UserToken(); + token2.setUserId(token1.getUserId()); + token2.setToken(UUIDs.endOf(System.currentTimeMillis() + 100)); + token2.setUserComment("bar"); + + template.insert(token1); + template.insert(token2); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())).sort(Sort.by("token")); + List loaded = template.select(query, UserToken.class); + + assertThat(loaded).containsSequence(token1, token2); + } + + @Test // DATACASS-343 + public void shouldSelectOneByQuery() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + template.insert(token1); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())); + UserToken loaded = template.selectOne(query, UserToken.class); + + assertThat(loaded).isEqualTo(token1); + } + @Test // DATACASS-292 public void insertShouldInsertEntity() { @@ -98,6 +171,46 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(template.selectOneById(person.getId(), Person.class)).isEqualTo(person); } + @Test // DATACASS-343 + public void updateShouldUpdateEntityByQuery() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + boolean result = template.update(query, Update.empty().set("firstname", "Walter Hartwell"), Person.class); + assertThat(result).isTrue(); + + assertThat(template.selectOneById(person.getId(), Person.class).getFirstname()).isEqualTo("Walter Hartwell"); + } + + @Test // DATACASS-343 + public void deleteByQueryShouldRemoveEntity() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + assertThat(template.delete(query, Person.class)).isTrue(); + + assertThat(template.selectOneById(person.getId(), Person.class)).isNull(); + } + + @Test // DATACASS-343 + public void deleteColumnsByQueryShouldRemoveColumn() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person); + + Query query = Query.query(Criteria.where("id").is("heisenberg")).columns(Columns.from("lastname")); + + assertThat(template.delete(query, Person.class)).isTrue(); + + Person loaded = template.selectOneById(person.getId(), Person.class); + assertThat(loaded.getFirstname()).isEqualTo("Walter"); + assertThat(loaded.getLastname()).isNull(); + } + @Test // DATACASS-292 public void deleteShouldRemoveEntity() { @@ -133,6 +246,19 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(stream.collect(Collectors.toList())).hasSize(1).contains(person); } + @Test // DATACASS-343 + public void streamByQuery() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + + Stream stream = template.stream(query, Person.class); + + assertThat(stream.collect(Collectors.toList())).hasSize(1).contains(person); + } + @Test // DATACASS-182 public void updateShouldRemoveFields() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java index 5591c53ee..a17a44eee 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateIntegrationTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.core; +import static org.assertj.core.api.Assertions.*; + import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -25,8 +27,16 @@ import org.springframework.cassandra.core.ReactiveCqlTemplate; import org.springframework.cassandra.core.session.DefaultBridgedReactiveSession; import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest; import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.domain.UserToken; import org.springframework.data.cassandra.test.integration.support.SchemaTestUtils; +import org.springframework.data.domain.Sort; + +import com.datastax.driver.core.utils.UUIDs; /** * Integration tests for {@link ReactiveCassandraTemplate}. @@ -47,7 +57,9 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC template = new ReactiveCassandraTemplate(new ReactiveCqlTemplate(session), converter); SchemaTestUtils.potentiallyCreateTableFor(Person.class, cassandraTemplate); + SchemaTestUtils.potentiallyCreateTableFor(UserToken.class, cassandraTemplate); SchemaTestUtils.truncate(Person.class, cassandraTemplate); + SchemaTestUtils.truncate(UserToken.class, cassandraTemplate); } @Test // DATACASS-335 @@ -87,6 +99,48 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.selectOneById(person.getId(), Person.class)).expectNext(person).verifyComplete(); } + @Test // DATACASS-343 + public void updateShouldUpdateEntityByQuery() { + + Person person = new Person("heisenberg", "Walter", "White"); + + template.insert(person).block(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + boolean result = template.update(query, Update.empty().set("firstname", "Walter Hartwell"), Person.class).block(); + assertThat(result).isTrue(); + + assertThat(template.selectOneById(person.getId(), Person.class).block().getFirstname()) + .isEqualTo("Walter Hartwell"); + } + + @Test // DATACASS-343 + public void deleteByQueryShouldRemoveEntity() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person).block(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")); + assertThat(template.delete(query, Person.class).block()).isTrue(); + + assertThat(template.selectOneById(person.getId(), Person.class).block()).isNull(); + } + + @Test // DATACASS-343 + public void deleteColumnsByQueryShouldRemoveColumn() { + + Person person = new Person("heisenberg", "Walter", "White"); + template.insert(person).block(); + + Query query = Query.query(Criteria.where("id").is("heisenberg")).columns(Columns.from("lastname")); + + assertThat(template.delete(query, Person.class).block()).isTrue(); + + Person loaded = template.selectOneById(person.getId(), Person.class).block(); + assertThat(loaded.getFirstname()).isEqualTo("Walter"); + assertThat(loaded.getLastname()).isNull(); + } + @Test // DATACASS-335 public void deleteShouldRemoveEntity() { @@ -110,4 +164,40 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.selectOneById(person.getId(), Person.class)).verifyComplete(); } + + @Test // DATACASS-343 + public void shouldSelectByQueryWithSorting() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + UserToken token2 = new UserToken(); + token2.setUserId(token1.getUserId()); + token2.setToken(UUIDs.endOf(System.currentTimeMillis() + 100)); + token2.setUserComment("bar"); + + template.insert(token1).block(); + template.insert(token2).block(); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())).sort(Sort.by("token")); + + assertThat(template.select(query, UserToken.class).collectList().block()).containsSequence(token1, token2); + } + + @Test // DATACASS-343 + public void shouldSelectOneByQuery() { + + UserToken token1 = new UserToken(); + token1.setUserId(UUIDs.endOf(System.currentTimeMillis())); + token1.setToken(UUIDs.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + template.insert(token1).block(); + + Query query = Query.query(Criteria.where("userId").is(token1.getUserId())); + + assertThat(template.selectOne(query, UserToken.class).block()).isEqualTo(token1); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java new file mode 100644 index 000000000..17b7cd0e4 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java @@ -0,0 +1,244 @@ +/* + * Copyright 2017 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.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +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.convert.UpdateMapper; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.data.cassandra.domain.Group; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.domain.Sort; + +import com.datastax.driver.core.Statement; + +/** + * Unit tests for {@link StatementFactory}. + * + * @author Mark Paluch + */ +public class StatementFactoryUnitTests { + + CassandraConverter converter = new MappingCassandraConverter(); + + UpdateMapper updateMapper = new UpdateMapper(converter); + + StatementFactory statementFactory = new StatementFactory(updateMapper, updateMapper); + + CassandraPersistentEntity groupEntity = converter.getMappingContext().getRequiredPersistentEntity(Group.class); + CassandraPersistentEntity personEntity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); + + @Test // DATACASS-343 + public void shouldMapSimpleSelectQuery() { + + Statement select = statementFactory.select(Query.empty(), + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(select.toString()).isEqualTo("SELECT * FROM group;"); + } + + @Test // DATACASS-343 + public void shouldMapSelectQueryWithColumnsAndCriteria() { + + Query query = Query.query(Criteria.where("foo").is("bar")).columns(Columns.from("age")); + + Statement select = statementFactory.select(query, groupEntity); + + assertThat(select.toString()).isEqualTo("SELECT age FROM group WHERE foo='bar';"); + } + + @Test // DATACASS-343 + public void shouldMapSelectQueryWithTtlColumns() { + + Query query = Query.empty().columns(Columns.empty().ttl("email")); + + Statement select = statementFactory.select(query, + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(select.toString()).isEqualTo("SELECT TTL(email) FROM group;"); + } + + @Test // DATACASS-343 + public void shouldMapSelectQueryWithSortLimitAndAllowFiltering() { + + Query query = Query.empty().sort(Sort.by("id.hashPrefix")).limit(10).withAllowFiltering(); + + Statement select = statementFactory.select(query, + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(select.toString()).isEqualTo("SELECT * FROM group ORDER BY hash_prefix ASC LIMIT 10 ALLOW FILTERING;"); + } + + @Test // DATACASS-343 + public void shouldMapDeleteQueryWithColumns() { + + Query query = Query.empty().columns(Columns.from("age")); + + Statement delete = statementFactory.delete(query, + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(delete.toString()).isEqualTo("DELETE age FROM group;"); + } + + @Test // DATACASS-343 + public void shouldMapDeleteQueryWithTtlColumns() { + + Query query = Query.query(Criteria.where("foo").is("bar")); + + Statement delete = statementFactory.delete(query, + converter.getMappingContext().getRequiredPersistentEntity(Group.class)); + + assertThat(delete.toString()).isEqualTo("DELETE FROM group WHERE foo='bar';"); + } + + @Test // DATACASS-343 + public void shouldCreateSetUpdate() { + + Query query = Query.query(Criteria.where("foo").is("bar")); + + Statement update = statementFactory.update(query, Update.empty().set("firstName", "baz").set("boo", "baa"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET first_name='baz',boo='baa' WHERE foo='bar';"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtIndexUpdate() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().set("list").atIndex(10).to("Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET list[10]='Euro';"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtKeyUpdate() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().set("map").atKey("baz").to("Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET map['baz']='Euro';"); + } + + @Test // DATACASS-343 + public void shouldAddToMap() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().addTo("map").entry("foo", "Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET map=map+{'foo':'Euro'};"); + } + + @Test // DATACASS-343 + public void shouldPrependAllToList() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().addTo("list").prependAll("foo", "Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET list=['foo','Euro']+list;"); + } + + @Test // DATACASS-343 + public void shouldAppendAllToList() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().addTo("list").appendAll("foo", "Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET list=list+['foo','Euro'];"); + } + + @Test // DATACASS-343 + public void shouldRemoveFromList() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().remove("list", "Euro"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET list=list-['Euro'];"); + } + + @Test // DATACASS-343 + public void shouldClearList() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().clear("list"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET list=[];"); + } + + @Test // DATACASS-343 + public void shouldAddAllToSet() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().addTo("set").appendAll("foo", "Euro"), + personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET set_col=set_col+{'foo','Euro'};"); + } + + @Test // DATACASS-343 + public void shouldRemoveFromSet() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().remove("set", "Euro"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET set_col=set_col-{'Euro'};"); + } + + @Test // DATACASS-343 + public void shouldClearSet() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().clear("set"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET set_col={};"); + } + + @Test // DATACASS-343 + public void shouldCreateIncrementUpdate() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().increment("number"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET number=number+1;"); + } + + @Test // DATACASS-343 + public void shouldCreateDecrementUpdate() { + + Statement update = statementFactory.update(Query.empty(), Update.empty().decrement("number"), personEntity); + + assertThat(update.toString()).isEqualTo("UPDATE person SET number=number-1;"); + } + + static class Person { + + @Id String id; + + List list; + @Column("set_col") Set set; + Map map; + + Integer number; + + @Column("first_name") String firstName; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnNameUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnNameUnitTests.java new file mode 100644 index 000000000..857074f57 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnNameUnitTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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.core.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.cassandra.core.cql.CqlIdentifier; + +/** + * Unit tests for {@link ColumnName}. + * + * @author Mark Paluch + */ +public class ColumnNameUnitTests { + + @Test // DATACASS-343 + public void stringBasedShouldEqual() { + + ColumnName first = ColumnName.from("foo"); + ColumnName second = ColumnName.from("foo"); + ColumnName different = ColumnName.from("Foo"); + + assertThat(first).isEqualTo(second); + assertThat(first.equals(second)).isTrue(); + assertThat(first.hashCode()).isEqualTo(second.hashCode()); + + assertThat(first).isNotEqualTo(different); + assertThat(first.equals(different)).isFalse(); + assertThat(first.hashCode()).isNotEqualTo(different.hashCode()); + } + + @Test // DATACASS-343 + public void cqlBasedShouldEqual() { + + ColumnName first = ColumnName.from(CqlIdentifier.cqlId("foo")); + ColumnName second = ColumnName.from(CqlIdentifier.cqlId("Foo")); + + ColumnName different = ColumnName.from(CqlIdentifier.cqlId("Foo", true)); + + assertThat(first).isEqualTo(second); + assertThat(first.equals(second)).isTrue(); + assertThat(first.hashCode()).isEqualTo(second.hashCode()); + + assertThat(first).isNotEqualTo(different); + assertThat(first.equals(different)).isFalse(); + assertThat(first.hashCode()).isNotEqualTo(different.hashCode()); + } + + @Test // DATACASS-343 + public void stringAndCqlComparisonShouldEqual() { + + ColumnName first = ColumnName.from("foo"); + ColumnName second = ColumnName.from(CqlIdentifier.cqlId("foo")); + ColumnName different = ColumnName.from(CqlIdentifier.cqlId("one", true)); + + assertThat(first).isEqualTo(second); + assertThat(first.equals(second)).isTrue(); + assertThat(first.hashCode()).isEqualTo(second.hashCode()); + + assertThat(first).isNotEqualTo(different); + assertThat(first.equals(different)).isFalse(); + assertThat(first.hashCode()).isNotEqualTo(different.hashCode()); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnsUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnsUnitTests.java new file mode 100644 index 000000000..b263898f0 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/ColumnsUnitTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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.core.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.cassandra.core.cql.CqlIdentifier; + +/** + * Unit tests for {@link Columns}. + * + * @author Mark Paluch + */ +public class ColumnsUnitTests { + + @Test // DATACASS-343 + public void shouldCreateEmpty() { + + Columns columns = Columns.empty(); + + assertThat(columns.toString()).isEqualTo("*"); + } + + @Test // DATACASS-343 + public void shouldIncludeColumn() { + + Columns columns = Columns.empty().include("foo").include("bar").ttl("baz"); + + assertThat(columns.toString()).isEqualTo("foo, bar, TTL(baz)"); + } + + @Test // DATACASS-343 + public void shouldCreateFromColumns() { + + Columns columns = Columns.from("asc", "bar"); + + assertThat(columns.toString()).isEqualTo("asc, bar"); + } + + @Test // DATACASS-343 + public void shouldCreateFromCqlIdentifiers() { + + Columns columns = Columns.from(CqlIdentifier.cqlId("Foo", true), CqlIdentifier.cqlId("bar")); + + assertThat(columns.toString()).contains("\"Foo\"").contains("bar"); + } + + @Test // DATACASS-343 + public void shouldBeEquals() { + + Columns columns = Columns.empty().include("foo").include("bar"); + Columns other = Columns.from("foo", "bar"); + Columns differentOrder = Columns.from("bar", "foo"); + Columns withTtl = Columns.empty().include("foo").include("bar").ttl("baz"); + + assertThat(columns.equals(other)).isTrue(); + assertThat(columns.equals(differentOrder)).isTrue(); + assertThat(columns.equals(withTtl)).isFalse(); + assertThat(withTtl.equals(columns)).isFalse(); + + assertThat(columns.hashCode()).isEqualTo(other.hashCode()); + assertThat(columns.hashCode()).isNotEqualTo(withTtl.hashCode()); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/CriteriaUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/CriteriaUnitTests.java new file mode 100644 index 000000000..2da8905a9 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/CriteriaUnitTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 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.core.query; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.cassandra.core.query.SerializationUtils.*; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Test; + +/** + * Unit tests for {@link Criteria}. + * + * @author Mark Paluch + */ +public class CriteriaUnitTests { + + @Test // DATACASS-343 + public void shouldCreateIsEqualTo() { + + CriteriaDefinition criteria = Criteria.where("foo").is("bar"); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo = 'bar'"); + } + + @Test // DATACASS-343 + public void shouldCreateIsGreater() { + + CriteriaDefinition criteria = Criteria.where("foo").gt(17); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo > 17"); + } + + @Test // DATACASS-343 + public void shouldCreateIsGreaterOrEquals() { + + CriteriaDefinition criteria = Criteria.where("foo").gte(17); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo >= 17"); + } + + @Test // DATACASS-343 + public void shouldCreateIsLess() { + + CriteriaDefinition criteria = Criteria.where("foo").lt(17); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo < 17"); + } + + @Test // DATACASS-343 + public void shouldCreateIsLessOrEquals() { + + CriteriaDefinition criteria = Criteria.where("foo").lte(17); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo <= 17"); + } + + @Test // DATACASS-343 + public void shouldCreateIsInArray() { + + CriteriaDefinition criteria = Criteria.where("foo").in("a", "b", "c"); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo IN ['a','b','c']"); + } + + @Test // DATACASS-343 + public void shouldCreateIsInCollection() { + + CriteriaDefinition criteria = Criteria.where("foo").in(Arrays.asList("a", "b", "c")); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo IN ['a','b','c']"); + } + + @Test // DATACASS-343 + public void shouldCreateIsInListOfObject() { + + CriteriaDefinition criteria = Criteria.where("foo").in(Arrays.asList("a", "b", new Object())); + + assertThat(serializeToCqlSafely(criteria)).startsWith("foo IN ['a','b',java.lang.Object@"); + } + + @Test // DATACASS-343 + public void shouldCreateIsInSet() { + + CriteriaDefinition criteria = Criteria.where("foo").in(new HashSet<>(Arrays.asList("a", "b", "c"))); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo IN {'a','b','c'}"); + } + + @Test // DATACASS-343 + public void shouldCreateLike() { + + CriteriaDefinition criteria = Criteria.where("foo").like("a%"); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo LIKE 'a%'"); + } + + @Test // DATACASS-343 + public void shouldCreateContains() { + + CriteriaDefinition criteria = Criteria.where("foo").contains("a"); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo CONTAINS 'a'"); + } + + @Test // DATACASS-343 + public void shouldCreateContainsKey() { + + CriteriaDefinition criteria = Criteria.where("foo").containsKey("a"); + + assertThat(serializeToCqlSafely(criteria)).isEqualTo("foo CONTAINS KEY 'a'"); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/QueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/QueryUnitTests.java new file mode 100644 index 000000000..42bc2832e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/QueryUnitTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 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.core.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.data.domain.Sort; + +/** + * Unit tests for {@link Query}. + * + * @author Mark Paluch + */ +public class QueryUnitTests { + + @Test // DATACASS-343 + public void shouldCreateFromChainedCriteria() { + + Query query = Query.query(Criteria.where("userId").is("foo")).and(Criteria.where("userComment").is("bar")); + + assertThat(query).hasSize(2); + assertThat(query.getCriteriaDefinitions()).contains(Criteria.where("userId").is("foo")); + assertThat(query.getCriteriaDefinitions()).contains(Criteria.where("userComment").is("bar")); + } + + @Test // DATACASS-343 + public void shouldRepresentQueryToString() { + + Query query = Query.query(Criteria.where("userId").is("foo")).and(Criteria.where("userComment").is("bar")) + .sort(Sort.by("foo", "bar")) // + .columns(Columns.from("foo").ttl("bar")) // + .limit(5); + + assertThat(query.toString()).isEqualTo( + "Query: userId = 'foo' AND userComment = 'bar', Columns: foo, TTL(bar), Sort: foo: ASC,bar: ASC, Limit: 5"); + } + + @Test // DATACASS-343 + public void shouldConfigureQueryObject() { + + Query query = Query.query(Criteria.where("foo").is("bar")); + Sort sort = Sort.by("a", "b"); + Columns columns = Columns.from("a", "b"); + + query = query.sort(sort).columns(columns).limit(10).withAllowFiltering(); + + assertThat(query).hasSize(1); + assertThat(query.getColumns()).isEqualTo(columns); + assertThat(query.getSort()).isEqualTo(sort); + assertThat(query.getLimit()).isEqualTo(10); + assertThat(query.isAllowFiltering()).isTrue(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/UpdateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/UpdateUnitTests.java new file mode 100644 index 000000000..e77e990e2 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/query/UpdateUnitTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2017 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.core.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.data.cassandra.core.query.Update.IncrOp; + +/** + * Unit tests for {@link Update}. + * + * @author Mark Paluch + */ +public class UpdateUnitTests { + + @Test // DATACASS-343 + public void shouldCreateSimpleUpdate() { + + Update update = Update.update("foo", "bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = 'bar'"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtIndexUpdate() { + + Update update = Update.empty().set("foo").atIndex(10).to("bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo[10] = 'bar'"); + } + + @Test // DATACASS-343 + public void shouldCreateSetAtKeyUpdate() { + + Update update = Update.empty().set("foo").atKey("baz").to("bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo['baz'] = 'bar'"); + } + + @Test // DATACASS-343 + public void shouldAddToMap() { + + Update update = Update.empty().addTo("foo").entry("foo", "bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = foo + {'foo':'bar'}"); + } + + @Test // DATACASS-343 + public void shouldPrependAllToList() { + + Update update = Update.empty().addTo("foo").prependAll("foo", "bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = ['foo','bar'] + foo"); + } + + @Test // DATACASS-343 + public void shouldAppendAllToList() { + + Update update = Update.empty().addTo("foo").appendAll("foo", "bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = foo + ['foo','bar']"); + } + + @Test // DATACASS-343 + public void shouldRemoveFromList() { + + Update update = Update.empty().remove("foo", "bar"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = foo - ['bar']"); + } + + @Test // DATACASS-343 + public void shouldClearCollection() { + + Update update = Update.empty().clear("foo"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = []"); + } + + @Test // DATACASS-343 + public void shouldCreateIncrementUpdate() { + + Update update = Update.empty().increment("foo"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = foo + 1"); + } + + @Test // DATACASS-343 + public void shouldCreateDecrementUpdate() { + + Update update = Update.empty().decrement("foo"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("foo = foo - 1"); + } + + @Test // DATACASS-343 + public void shouldCreateUpdateForTwoColumns() { + + Update update = Update.empty().increment("foo").decrement("bar"); + + assertThat(update.getUpdateOperations()).hasSize(2); + assertThat(update.toString()).isEqualTo("foo = foo + 1, bar = bar - 1"); + } + + @Test // DATACASS-343 + public void shouldCreateSingleUpdateForTheSameColumn() { + + Update update = Update.empty().set("foo", "bar").decrement("foo"); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.getUpdateOperations().iterator().next()).isInstanceOf(IncrOp.class); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/CompositeKey.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/CompositeKey.java index 671e5b8c1..73118d3d3 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/CompositeKey.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/CompositeKey.java @@ -30,6 +30,6 @@ import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; @Data public class CompositeKey implements Serializable { - @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1) private String firstname; + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1, name = "first_name") private String firstname; @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 2) private String lastname; } 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 index 59cfb404a..e91f1505f 100644 --- 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 @@ -33,6 +33,9 @@ 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.convert.UpdateMapper; +import org.springframework.data.cassandra.core.StatementFactory; +import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.domain.Person; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; @@ -45,6 +48,8 @@ import org.springframework.data.cassandra.mapping.Table; import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; import org.springframework.data.repository.query.parser.PartTree; +import com.datastax.driver.core.RegularStatement; + /** * Unit tests for {@link CassandraQueryCreator}. * @@ -268,9 +273,13 @@ public class CassandraQueryCreatorUnitTests { 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(); + CassandraQueryCreator creator = new CassandraQueryCreator(tree, getAccessor(converter, values), context); + + StatementFactory factory = new StatementFactory(new UpdateMapper(converter)); + Query query = creator.createQuery(); + + RegularStatement select = factory.select(query, context.getRequiredPersistentEntity(entityClass)); + return select.toString(); } @SuppressWarnings("unchecked") diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java index e43c0919e..6a13c8cda 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -34,6 +34,7 @@ import org.springframework.cassandra.core.cql.CqlIdentifier; 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.Group; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.UserTypeResolver; import org.springframework.data.cassandra.repository.CassandraRepository; @@ -45,6 +46,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.Statement; import com.datastax.driver.core.UDTValue; import com.datastax.driver.core.UserType; @@ -109,8 +111,9 @@ public class PartTreeCassandraQueryUnitTests { @Test // DATACASS-357 public void shouldDeriveFieldInCollectionQuery() { - String query = deriveQueryFromMethod("findByFirstnameIn", new Class[] { Collection.class }, - Arrays.asList("Hank", "Walter")); + + String query = deriveQueryFromMethod(Repo.class, "findByFirstnameIn", new Class[] { Collection.class }, + Arrays.asList("Hank", "Walter")).toString(); assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname IN ('Hank','Walter');"); } @@ -137,12 +140,21 @@ public class PartTreeCassandraQueryUnitTests { @Test // DATACASS-357 public void shouldDeriveUdtInCollectionQuery() { - String query = deriveQueryFromMethod("findByMainAddressIn", new Class[] { Collection.class }, - Collections.singleton(udtValueMock)); + String query = deriveQueryFromMethod(Repo.class, "findByMainAddressIn", new Class[] { Collection.class }, + Collections.singleton(udtValueMock)).toString(); assertThat(query).isEqualTo("SELECT * FROM person WHERE mainaddress IN ({});"); } + @Test // DATACASS-343 + public void shouldRenderMappedColumnNamesForCompositePrimaryKey() { + + Statement query = deriveQueryFromMethod(GroupRepository.class, "findByIdHashPrefix", new Class[] { String.class }, + "foo"); + + assertThat(query.toString()).isEqualTo("SELECT * FROM group WHERE hash_prefix='foo';"); + } + private String deriveQueryFromMethod(String method, Object... args) { Class[] types = new Class[args.length]; @@ -151,12 +163,13 @@ public class PartTreeCassandraQueryUnitTests { types[i] = ClassUtils.getUserClass(args[i].getClass()); } - return deriveQueryFromMethod(method, types, args); + return deriveQueryFromMethod(Repo.class, method, types, args).toString(); } - private String deriveQueryFromMethod(String method, Class[] types, Object... args) { + private Statement deriveQueryFromMethod(Class repositoryInterface, String method, Class[] types, + Object... args) { - PartTreeCassandraQuery partTreeQuery = createQueryForMethod(method, types); + PartTreeCassandraQuery partTreeQuery = createQueryForMethod(repositoryInterface, method, types); CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(), args); @@ -164,17 +177,14 @@ public class PartTreeCassandraQueryUnitTests { return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor)); } - private PartTreeCassandraQuery createQueryForMethod(String methodName, Class... paramTypes) { - - Class[] userTypes = Arrays.stream(paramTypes)// + private PartTreeCassandraQuery createQueryForMethod(Class repositoryInterface,String methodName, Class... paramTypes) {Class[] userTypes = Arrays.stream(paramTypes)// .map(it -> it.getName().contains("Mockito") ? it.getSuperclass() : it)// .toArray(size -> new Class[size]); - try { - Method method = Repo.class.getMethod(methodName, userTypes); + Method method = repositoryInterface.getMethod(methodName, userTypes); ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); - CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), - factory, mappingContext); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, + new DefaultRepositoryMetadata(repositoryInterface), factory, mappingContext); return new PartTreeCassandraQuery(queryMethod, mockCassandraOperations); } catch (NoSuchMethodException e) { @@ -184,6 +194,12 @@ public class PartTreeCassandraQueryUnitTests { } } + @SuppressWarnings("unused") + interface GroupRepository extends CassandraRepository { + + Group findByIdHashPrefix(String hashPrefix); + } + @SuppressWarnings("unused") interface Repo extends CassandraRepository { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java index 68b9514f1..cc15f435f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java @@ -95,6 +95,7 @@ public class ReactivePartTreeCassandraQueryUnitTests { } private String deriveQueryFromMethod(String method, Object... args) { + Class[] types = new Class[args.length]; for (int i = 0; i < args.length; i++) { @@ -106,7 +107,8 @@ public class ReactivePartTreeCassandraQueryUnitTests { CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(), args); - return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor)); + return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor)) + .toString(); } private ReactivePartTreeCassandraQuery createQueryForMethod(String methodName, Class... paramTypes) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java index 327f8d355..24fac6d18 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactiveStringBasedCassandraQueryUnitTests.java @@ -16,8 +16,6 @@ package org.springframework.data.cassandra.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -27,7 +25,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cassandra.core.ReactiveCqlOperations; -import org.springframework.cassandra.core.ReactiveSessionCallback; import org.springframework.cassandra.core.session.ReactiveSession; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; @@ -44,11 +41,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ReflectionUtils; import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.Configuration; import com.datastax.driver.core.SimpleStatement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; /** * Unit tests for {@link StringBasedCassandraQuery}. @@ -74,13 +68,6 @@ public class ReactiveStringBasedCassandraQueryUnitTests { @SuppressWarnings("unchecked") public void setUp() { - when(operations.getReactiveCqlOperations()).thenReturn(cqlOperations); - when(cqlOperations.execute(any(ReactiveSessionCallback.class))).thenAnswer( - invocation -> ((ReactiveSessionCallback) invocation.getArguments()[0]).doInSession(reactiveSession)); - when(reactiveSession.getCluster()).thenReturn(cluster); - when(cluster.getConfiguration()).thenReturn(configuration); - when(configuration.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT_INSTANCE); - this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); this.factory = new SpelAwareProxyProjectionFactory(); @@ -95,16 +82,10 @@ public class ReactiveStringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "White"); - String stringQuery = cassandraQuery.createQuery(accessor); - SimpleStatement actual = new SimpleStatement(stringQuery); + SimpleStatement stringQuery = cassandraQuery.createQuery(accessor); - String table = Person.class.getSimpleName().toLowerCase(); - Select expected = QueryBuilder.select().all().from(table); - - expected.setForceNoValues(true); - expected.where(QueryBuilder.eq("lastname", "White")); - - assertThat(actual.getQueryString()).isEqualTo(expected.getQueryString()); + assertThat(stringQuery.toString()).isEqualTo("SELECT * FROM person WHERE lastname=?;"); + assertThat(stringQuery.getObject(0)).isEqualTo("White"); } private ReactiveStringBasedCassandraQuery getQueryMethod(String name, Class... args) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java index 40581a42c..1f08bfd8b 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java @@ -16,14 +16,11 @@ package org.springframework.data.cassandra.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.time.LocalDate; import java.util.Arrays; @@ -35,14 +32,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.cassandra.core.CqlOperations; -import org.springframework.cassandra.core.SessionCallback; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.UserTypeResolver; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.support.UserTypeBuilder; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.projection.ProjectionFactory; @@ -56,17 +52,10 @@ import org.springframework.data.repository.query.QueryCreationException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ReflectionUtils; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.Configuration; import com.datastax.driver.core.DataType; -import com.datastax.driver.core.ProtocolVersion; -import com.datastax.driver.core.Session; +import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.UDTValue; import com.datastax.driver.core.UserType; -import com.datastax.driver.core.UserType.Field; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; /** * Unit tests for {@link StringBasedCassandraQuery}. @@ -81,10 +70,6 @@ public class StringBasedCassandraQueryUnitTests { SpelExpressionParser PARSER = new SpelExpressionParser(); @Mock CassandraOperations operations; - @Mock CqlOperations cqlOperations; - @Mock Session session; - @Mock Cluster cluster; - @Mock Configuration configuration; @Mock UserTypeResolver userTypeResolver; @Mock UDTValue udtValue; @@ -99,13 +84,6 @@ public class StringBasedCassandraQueryUnitTests { BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); mappingContext.setUserTypeResolver(userTypeResolver); - when(operations.getCqlOperations()).thenReturn(cqlOperations); - when(cqlOperations.execute(any(SessionCallback.class))) - .thenAnswer(invocation -> ((SessionCallback) invocation.getArguments()[0]).doInSession(session)); - when(session.getCluster()).thenReturn(cluster); - when(cluster.getConfiguration()).thenReturn(configuration); - when(configuration.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT_INSTANCE); - this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.converter = new MappingCassandraConverter(mappingContext); this.factory = new SpelAwareProxyProjectionFactory(); @@ -120,9 +98,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); } @Test // DATACASS-259 @@ -132,9 +111,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); } @Test // DATACASS-117 @@ -144,9 +124,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Mat\th'ew\"s"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Mat\th''ew\"s';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Mat\th'ew\"s"); } @Test // DATACASS-117 @@ -156,9 +137,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), ByteBuffer.wrap(new byte[] { 1, 2, 3, 4 })); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 0x01020304;"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo(ByteBuffer.wrap(new byte[] { 1, 2, 3, 4 })); } @Test // DATACASS-117 @@ -168,9 +150,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), Arrays.asList("White", "Heisenberg")); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname IN ('White','Heisenberg');"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname IN (?);"); + assertThat(actual.getObject(0)).isEqualTo(Arrays.asList("White", "Heisenberg")); } @Test // DATACASS-117 @@ -180,9 +163,11 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), Arrays.asList("White", "Heisenberg"), 42); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastnames = ['White','Heisenberg'] AND age = 42;"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastnames = [?] AND age = ?;"); + assertThat(actual.getObject(0)).isEqualTo(Arrays.asList("White", "Heisenberg")); + assertThat(actual.getObject(1)).isEqualTo(42); } @Test(expected = QueryCreationException.class) // DATACASS-117 @@ -212,9 +197,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), new HashSet<>(Arrays.asList("White", "Heisenberg"))); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname IN ('White','Heisenberg');"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname IN (?);"); + assertThat(actual.getObject(0)).isEqualTo(new HashSet(Arrays.asList("White", "Heisenberg"))); } @Test // DATACASS-117 @@ -224,9 +210,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Walter", "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); } @Test // DATACASS-117 @@ -236,9 +223,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); } @Test // DATACASS-117 @@ -248,9 +236,10 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); } @Test // DATACASS-117 @@ -260,15 +249,17 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Woohoo';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Woohoo"); accessor = new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), "Walter"); actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Walter';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Walter"); } @Test // DATACASS-117 @@ -278,9 +269,11 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname='Matthews' or firstname = 'Matthews';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname = ? or firstname = ?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); + assertThat(actual.getObject(1)).isEqualTo("Matthews"); } @Test // DATACASS-117 @@ -290,14 +283,11 @@ public class StringBasedCassandraQueryUnitTests { CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( cassandraQuery.getQueryMethod(), "Matthews", "John"); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - String table = Person.class.getSimpleName().toLowerCase(); - Select expected = QueryBuilder.select().all().from(table); - expected.setForceNoValues(true); - expected.where(QueryBuilder.eq("lastname", "Matthews")).and(QueryBuilder.eq("firstname", "John")); - - assertThat(actual).isEqualTo(expected.getQueryString()); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE lastname=? AND firstname=?;"); + assertThat(actual.getObject(0)).isEqualTo("Matthews"); + assertThat(actual.getObject(1)).isEqualTo("John"); } @Test // DATACASS-296 @@ -307,17 +297,18 @@ public class StringBasedCassandraQueryUnitTests { CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), LocalDate.of(2010, 7, 4))); - String actual = cassandraQuery.createQuery(accessor); + SimpleStatement actual = cassandraQuery.createQuery(accessor); - assertThat(actual).isEqualTo("SELECT * FROM person WHERE createdDate='2010-07-04';"); + assertThat(actual.toString()).isEqualTo("SELECT * FROM person WHERE createdDate=?;"); + assertThat(actual.getObject(0)).isInstanceOf(com.datastax.driver.core.LocalDate.class); + assertThat(actual.getObject(0).toString()).isEqualTo("2010-07-04"); } @Test // DATACASS-172 public void bindsMappedUdtPropertyCorrectly() throws Exception { - Field city = createField("city", DataType.varchar()); - Field country = createField("country", DataType.varchar()); - UserType addressType = createUserType("address", Arrays.asList(city, country)); + UserType addressType = UserTypeBuilder.forName("address").withField("city", DataType.varchar()) + .withField("country", DataType.varchar()).build(); when(userTypeResolver.resolveType(CqlIdentifier.cqlId("address"))).thenReturn(addressType); @@ -325,28 +316,23 @@ public class StringBasedCassandraQueryUnitTests { CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), new Address())); - String stringQuery = cassandraQuery.createQuery(accessor); + SimpleStatement stringQuery = cassandraQuery.createQuery(accessor); - // udtValueMock because that's the mock's UDTValue.toString() representation - assertThat(stringQuery).isEqualTo("SELECT * FROM person WHERE address={city:NULL,country:NULL};"); + assertThat(stringQuery.toString()).isEqualTo("SELECT * FROM person WHERE address=?;"); + assertThat(stringQuery.getObject(0).toString()).isEqualTo("{city:NULL,country:NULL}"); } @Test // DATACASS-172 public void bindsUdtValuePropertyCorrectly() throws Exception { - Field city = createField("city", DataType.varchar()); - Field country = createField("country", DataType.varchar()); - UserType addressType = createUserType("address", Arrays.asList(city, country)); - when(udtValue.getType()).thenReturn(addressType); - StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByMainAddress", UDTValue.class); CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), udtValue)); - String stringQuery = cassandraQuery.createQuery(accessor); + SimpleStatement stringQuery = cassandraQuery.createQuery(accessor); - // udtValueMock because that's the mock's UDTValue.toString() representation - assertThat(stringQuery).isEqualTo("SELECT * FROM person WHERE address={city:NULL,country:NULL};"); + assertThat(stringQuery.toString()).isEqualTo("SELECT * FROM person WHERE address=?;"); + assertThat(stringQuery.getObject(0).toString()).isEqualTo("udtValue"); } private StringBasedCassandraQuery getQueryMethod(String name, Class... args) { @@ -358,55 +344,12 @@ public class StringBasedCassandraQueryUnitTests { new ExtensionAwareEvaluationContextProvider()); } - private Field createField(String fieldName, DataType dataType) { - - try { - Constructor constructor = Field.class.getDeclaredConstructor(String.class, DataType.class); - constructor.setAccessible(true); - return constructor.newInstance(fieldName, dataType); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - @SuppressWarnings("unchecked") - private UserType createUserType(String typeName, Collection fields) { - - try { - - Constructor[] declaredConstructors = (Constructor[]) UserType.class.getDeclaredConstructors(); - for (Constructor constructor : declaredConstructors) { - - if (Modifier.isPrivate(constructor.getModifiers())) { - continue; - } - - constructor.setAccessible(true); - - if (constructor.getParameterCount() == 5) { - // Cassandra driver 3.0.x - 3.1.x - return constructor.newInstance(typeName, typeName, fields, ProtocolVersion.NEWEST_SUPPORTED, - CodecRegistry.DEFAULT_INSTANCE); - } - - // Cassandra driver 3.2.x - return constructor.newInstance(typeName, typeName, false, fields, ProtocolVersion.NEWEST_SUPPORTED, - CodecRegistry.DEFAULT_INSTANCE); - } - - } catch (Exception e) { - throw new IllegalStateException(e); - } - - throw new IllegalStateException("No suitable constructor found"); - } - private interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname = ?0;") Person findByLastname(String lastname); - @Query("SELECT * FROM person WHERE lastname=?0 or firstname = ?0;") + @Query("SELECT * FROM person WHERE lastname = ?0 or firstname = ?0;") Person findByLastnameUsedTwice(String lastname); @Query("SELECT * FROM person WHERE lastname = :lastname;") diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/UserTypeBuilder.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/UserTypeBuilder.java new file mode 100644 index 000000000..5b1a6bef0 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/UserTypeBuilder.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 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.support; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.cassandra.core.cql.CqlIdentifier; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.UserType; +import com.datastax.driver.core.UserType.Field; + +/** + * @author Mark Paluch + */ +public class UserTypeBuilder { + + private final CqlIdentifier typeName; + private List fields = new ArrayList<>(); + + private UserTypeBuilder(CqlIdentifier typeName) { + this.typeName = typeName; + } + + public static UserTypeBuilder forName(String typeName) { + return forName(CqlIdentifier.cqlId(typeName)); + } + + public static UserTypeBuilder forName(CqlIdentifier typeName) { + return new UserTypeBuilder(typeName); + } + + public UserTypeBuilder withField(String fieldName, DataType dataType) { + this.fields.add(createField(fieldName, dataType)); + return this; + } + + public UserType build() { + return createUserType(this.typeName.getUnquoted(), fields); + } + + private Field createField(String fieldName, DataType dataType) { + + try { + Constructor constructor = Field.class.getDeclaredConstructor(String.class, DataType.class); + constructor.setAccessible(true); + return constructor.newInstance(fieldName, dataType); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private UserType createUserType(String typeName, Collection fields) { + + try { + Constructor constructor = UserType.class.getDeclaredConstructor(String.class, String.class, + Boolean.TYPE, Collection.class, ProtocolVersion.class, CodecRegistry.class); + constructor.setAccessible(true); + return constructor.newInstance(typeName, typeName, false, fields, ProtocolVersion.NEWEST_SUPPORTED, + CodecRegistry.DEFAULT_INSTANCE); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +}