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); + } + } +}