Skip to content

Commit 1686a91

Browse files
committed
Allow multi-value collection removal and map entry removal by key.
We now accept multiple values when removing items from a collection. Additionally, we support now removal by key/keys for map columns. Resolves #1007.
1 parent e64f933 commit 1686a91

File tree

9 files changed

+233
-24
lines changed

9 files changed

+233
-24
lines changed

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Optional;
2626
import java.util.Set;
27+
import java.util.function.Function;
2728
import java.util.stream.Collectors;
2829

2930
import org.springframework.data.cassandra.core.convert.CassandraConverter;
@@ -800,27 +801,23 @@ private static Assignment getAssignment(SetOp updateOp, TermFactory termFactory)
800801
}
801802

802803
@SuppressWarnings("unchecked")
803-
private static Assignment getAssignment(RemoveOp updateOp, TermFactory termFactory) {
804+
private static Assignment getAssignment(RemoveOp removeOp, TermFactory termFactory) {
804805

805-
if (updateOp.getValue() instanceof Set) {
806-
807-
Collection<Object> collection = (Collection<Object>) updateOp.getValue();
806+
if (removeOp.getValue() instanceof Set) {
808807

809-
Assert.isTrue(collection.size() == 1, "RemoveOp must contain a single set element");
808+
Collection<Object> collection = (Collection<Object>) removeOp.getValue();
810809

811-
return Assignment.removeSetElement(updateOp.toCqlIdentifier(), termFactory.create(collection.iterator().next()));
810+
return new RemoveCollectionElementsAssignment(removeOp.toCqlIdentifier(), termFactory.create(collection));
812811
}
813812

814-
if (updateOp.getValue() instanceof List) {
815-
816-
Collection<Object> collection = (Collection<Object>) updateOp.getValue();
813+
if (removeOp.getValue() instanceof List) {
817814

818-
Assert.isTrue(collection.size() == 1, "RemoveOp must contain a single list element");
815+
Collection<Object> collection = (Collection<Object>) removeOp.getValue();
819816

820-
return Assignment.removeListElement(updateOp.toCqlIdentifier(), termFactory.create(collection.iterator().next()));
817+
return new RemoveCollectionElementsAssignment(removeOp.toCqlIdentifier(), termFactory.create(collection));
821818
}
822819

823-
return Assignment.remove(updateOp.toCqlIdentifier(), termFactory.create(updateOp.getValue()));
820+
return Assignment.remove(removeOp.toCqlIdentifier(), termFactory.create(removeOp.getValue()));
824821
}
825822

826823
private static Assignment getAssignment(AddToOp updateOp, TermFactory termFactory) {
@@ -1045,13 +1042,17 @@ private static Condition toCondition(CriteriaDefinition criteriaDefinition, Term
10451042
}
10461043

10471044
static List<Term> toLiterals(@Nullable Object arrayOrList) {
1045+
return toLiterals(arrayOrList, QueryBuilder::literal);
1046+
}
1047+
1048+
static List<Term> toLiterals(@Nullable Object arrayOrList, Function<Object, Term> termFactory) {
10481049

10491050
if (arrayOrList instanceof List) {
10501051

10511052
List<?> list = (List<?>) arrayOrList;
10521053
List<Term> literals = new ArrayList<>(list.size());
10531054
for (Object o : list) {
1054-
literals.add(QueryBuilder.literal(o));
1055+
literals.add(termFactory.apply(o));
10551056
}
10561057

10571058
return literals;
@@ -1062,7 +1063,7 @@ static List<Term> toLiterals(@Nullable Object arrayOrList) {
10621063
Object[] array = (Object[]) arrayOrList;
10631064
List<Term> literals = new ArrayList<>(array.length);
10641065
for (Object o : array) {
1065-
literals.add(QueryBuilder.literal(o));
1066+
literals.add(termFactory.apply(o));
10661067
}
10671068

10681069
return literals;
@@ -1096,4 +1097,37 @@ public void appendTo(@NonNull StringBuilder builder) {
10961097
builder.append(selector);
10971098
}
10981099
}
1100+
1101+
private static class RemoveCollectionElementsAssignment implements Assignment {
1102+
1103+
private final CqlIdentifier columnId;
1104+
private final Term value;
1105+
1106+
protected RemoveCollectionElementsAssignment(CqlIdentifier columnId, Term value) {
1107+
this.columnId = columnId;
1108+
this.value = value;
1109+
}
1110+
1111+
@Override
1112+
public void appendTo(StringBuilder builder) {
1113+
builder.append(String.format("%1$s=%1$s-%2$s", columnId.asCql(true), buildRightOperand()));
1114+
}
1115+
1116+
private String buildRightOperand() {
1117+
StringBuilder builder = new StringBuilder();
1118+
value.appendTo(builder);
1119+
return builder.toString();
1120+
}
1121+
1122+
@Override
1123+
public boolean isIdempotent() {
1124+
return value.isIdempotent();
1125+
}
1126+
1127+
public Term getValue() {
1128+
return value;
1129+
}
1130+
1131+
}
1132+
10991133
}

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/QueryMapper.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,17 @@ ColumnType transform(ColumnType typeDescriptor, CassandraPersistentProperty prop
398398
}
399399
},
400400

401+
/**
402+
* Wrap {@link ColumnType} into a set.
403+
*/
404+
ENCLOSING_SET {
405+
406+
@Override
407+
ColumnType transform(ColumnType typeDescriptor, CassandraPersistentProperty property) {
408+
return ColumnType.setOf(typeDescriptor);
409+
}
410+
},
411+
401412
/**
402413
* Use the map key type.
403414
*/
@@ -428,6 +439,17 @@ ColumnType transform(ColumnType typeDescriptor, CassandraPersistentProperty prop
428439

429440
return typeDescriptor;
430441
}
442+
},
443+
444+
/**
445+
* Wrap {@link ColumnType} into a set.
446+
*/
447+
ENCLOSING_MAP_KEY_SET {
448+
449+
@Override
450+
ColumnType transform(ColumnType typeDescriptor, CassandraPersistentProperty property) {
451+
return ColumnType.setOf(MAP_KEY_TYPE.transform(typeDescriptor, property));
452+
}
431453
};
432454

433455
/**

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UpdateMapper.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.cassandra.core.convert;
1717

18+
import static org.springframework.data.cassandra.core.query.Update.*;
19+
1820
import java.util.ArrayList;
1921
import java.util.Collection;
2022
import java.util.Collections;
@@ -28,14 +30,7 @@
2830
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
2931
import org.springframework.data.cassandra.core.query.Filter;
3032
import org.springframework.data.cassandra.core.query.Update;
31-
import org.springframework.data.cassandra.core.query.Update.AddToMapOp;
32-
import org.springframework.data.cassandra.core.query.Update.AddToOp;
33-
import org.springframework.data.cassandra.core.query.Update.AssignmentOp;
34-
import org.springframework.data.cassandra.core.query.Update.IncrOp;
35-
import org.springframework.data.cassandra.core.query.Update.RemoveOp;
36-
import org.springframework.data.cassandra.core.query.Update.SetAtIndexOp;
37-
import org.springframework.data.cassandra.core.query.Update.SetAtKeyOp;
38-
import org.springframework.data.cassandra.core.query.Update.SetOp;
33+
import org.springframework.data.cassandra.core.query.Update.*;
3934
import org.springframework.data.mapping.PersistentProperty;
4035
import org.springframework.data.util.TypeInformation;
4136
import org.springframework.util.Assert;
@@ -95,7 +90,7 @@ public Update getMappedObject(Update update, CassandraPersistentEntity<?> entity
9590
mapped.add(getMappedUpdateOperation(assignmentOp, field));
9691
}
9792

98-
return Update.of(mapped);
93+
return of(mapped);
9994
}
10095

10196
private AssignmentOp getMappedUpdateOperation(AssignmentOp assignmentOp, Field field) {
@@ -192,8 +187,21 @@ private AssignmentOp getMappedUpdateOperation(Field field, RemoveOp updateOp) {
192187

193188
Object value = updateOp.getValue();
194189
ColumnType descriptor = getColumnType(field, value, ColumnTypeTransformer.AS_IS);
190+
boolean mapLike = false;
191+
192+
if (field.getProperty().isPresent() && field.getProperty().get().isMapLike()) {
193+
194+
descriptor = getColumnType(field, value, value instanceof Collection ? ColumnTypeTransformer.ENCLOSING_MAP_KEY_SET
195+
: ColumnTypeTransformer.MAP_KEY_TYPE);
196+
mapLike = true;
197+
}
198+
195199
Object mappedValue = getConverter().convertToColumnType(value, descriptor);
196200

201+
if (mapLike && !(mappedValue instanceof Collection)) {
202+
mappedValue = Collections.singleton(mappedValue);
203+
}
204+
197205
return new RemoveOp(field.getMappedKey(), mappedValue);
198206
}
199207

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Update.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ public AddToBuilder addTo(String columnName) {
120120
return new DefaultAddToBuilder(ColumnName.from(columnName));
121121
}
122122

123+
/**
124+
* Create a new {@link RemoveFromBuilder} to remove items from a collection for {@code columnName} in a fluent style.
125+
*
126+
* @param columnName must not be {@literal null}.
127+
* @return a new {@link RemoveFromBuilder} to build an remove-from assignment.
128+
* @since 3.1.4
129+
*/
130+
public RemoveFromBuilder removeFrom(String columnName) {
131+
return new DefaultRemoveFromBuilder(ColumnName.from(columnName));
132+
}
133+
123134
/**
124135
* Remove {@code value} from the collection at {@code columnName}.
125136
*
@@ -403,6 +414,80 @@ public Update addAll(Map<?, ?> map) {
403414
}
404415
}
405416

417+
/**
418+
* Builder to remove a single element/multiple elements from a collection associated with a {@link ColumnName}.
419+
*
420+
* @author Mark Paluch
421+
* @since 3.1.4
422+
*/
423+
@SuppressWarnings("unused")
424+
public interface RemoveFromBuilder {
425+
426+
/**
427+
* Remove all entries matching {@code value} from a set, list or map (map key).
428+
*
429+
* @param value must not be {@literal null}.
430+
* @return a new {@link Update} object containing the merge result of the existing assignments and the current
431+
* assignment.
432+
*/
433+
Update value(Object value);
434+
435+
/**
436+
* Remove all entries matching {@code values} from a set, list or map (map key).
437+
*
438+
* @param values must not be {@literal null}.
439+
* @return a new {@link Update} object containing the merge result of the existing assignments and the current
440+
* assignment.
441+
*/
442+
default Update values(Object... values) {
443+
444+
Assert.notNull(values, "Values must not be null");
445+
446+
return values(Arrays.asList(values));
447+
}
448+
449+
/**
450+
* Remove all entries matching {@code values} from a set, list or map (map key).
451+
*
452+
* @param values must not be {@literal null}.
453+
* @return a new {@link Update} object containing the merge result of the existing assignments and the current
454+
* assignment.
455+
*/
456+
Update values(Iterable<? extends Object> values);
457+
458+
}
459+
460+
/**
461+
* Default {@link RemoveFromBuilder} implementation.
462+
*/
463+
private class DefaultRemoveFromBuilder implements RemoveFromBuilder {
464+
465+
private final ColumnName columnName;
466+
467+
DefaultRemoveFromBuilder(ColumnName columnName) {
468+
this.columnName = columnName;
469+
}
470+
471+
/* (non-Javadoc)
472+
* @see org.springframework.data.cassandra.core.query.Update.RemoveFromBuilder#mapValue(java.lang.Object)
473+
*/
474+
@Override
475+
public Update value(Object value) {
476+
return add(new RemoveOp(columnName, value));
477+
}
478+
479+
/* (non-Javadoc)
480+
* @see org.springframework.data.cassandra.core.query.Update.RemoveFromBuilder#mapValues(java.lang.Iterable)
481+
*/
482+
@Override
483+
public Update values(Iterable<?> values) {
484+
485+
Assert.notNull(values, "Values must not be null");
486+
487+
return add(new RemoveOp(columnName, values));
488+
}
489+
}
490+
406491
/**
407492
* Builder to associate a single value with a collection at a given index at {@link ColumnName}.
408493
*
@@ -718,4 +803,5 @@ public String toString() {
718803
return String.format("%s = %s - %s", getColumnName(), getColumnName(), serializeToCqlSafely(getValue()));
719804
}
720805
}
806+
721807
}

spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
import java.util.Arrays;
3131
import java.util.Collections;
3232
import java.util.HashSet;
33+
import java.util.LinkedHashMap;
3334
import java.util.LinkedHashSet;
3435
import java.util.List;
36+
import java.util.Map;
3537
import java.util.Set;
3638
import java.util.stream.Collectors;
3739
import java.util.stream.Stream;
@@ -562,6 +564,38 @@ void insertAndUpdateToEmptyCollection() {
562564
assertThat(loaded.getBookmarks()).isNull();
563565
}
564566

567+
@Test // #1007
568+
void updateCollection() {
569+
570+
BookReference bookReference = new BookReference();
571+
572+
bookReference.setIsbn("isbn");
573+
bookReference.setBookmarks(Arrays.asList(1, 2, 3, 4));
574+
bookReference.setReferences(new LinkedHashSet<>(Arrays.asList("one", "two", "three")));
575+
576+
Map<String, String> credits = new LinkedHashMap<>();
577+
credits.put("hello", "world");
578+
credits.put("other", "world");
579+
credits.put("external", "place");
580+
581+
bookReference.setCredits(credits);
582+
583+
template.insert(bookReference);
584+
585+
Query query = Query.query(where("isbn").is(bookReference.getIsbn()));
586+
587+
Update update = Update.empty().removeFrom("bookmarks").values(3, 4).removeFrom("references").values("one", "three")
588+
.removeFrom("credits").values("hello", "other", "place");
589+
590+
template.update(query, update, BookReference.class);
591+
592+
BookReference loaded = template.selectOneById(bookReference.getIsbn(), BookReference.class);
593+
594+
assertThat(loaded.getBookmarks()).containsOnly(1, 2);
595+
assertThat(loaded.getReferences()).containsOnly("two");
596+
assertThat(loaded.getCredits()).containsOnlyKeys("external");
597+
}
598+
565599
@Test // DATACASS-206
566600
void shouldUseSpecifiedColumnNamesForSingleEntityModifyingOperations() {
567601

spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/StatementFactoryUnitTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,20 @@ void shouldAddToMap() {
359359
assertThat(update.build(ParameterHandling.INLINE).getQuery()).isEqualTo("UPDATE person SET map=map+{'foo':'Euro'}");
360360
}
361361

362+
@Test // #1007
363+
void shouldRemoveFromMap() {
364+
365+
StatementBuilder<com.datastax.oss.driver.api.querybuilder.update.Update> update = statementFactory
366+
.update(Query.empty(), Update.empty().removeFrom("map").value("foo"), personEntity);
367+
368+
assertThat(update.build(ParameterHandling.INLINE).getQuery()).isEqualTo("UPDATE person SET map=map-{'foo'}");
369+
370+
update = statementFactory.update(Query.empty(), Update.empty().removeFrom("map").values("foo", "bar"),
371+
personEntity);
372+
373+
assertThat(update.build(ParameterHandling.INLINE).getQuery()).isEqualTo("UPDATE person SET map=map-{'foo','bar'}");
374+
}
375+
362376
@Test // DATACASS-343
363377
void shouldPrependAllToList() {
364378

spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/UpdateMapperUnitTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ void shouldAddToMap() {
179179
assertThat(update).hasToString("map = map + {'foo':'Euro'}");
180180
}
181181

182+
@Test // #1007
183+
void shouldRemoveFromMap() {
184+
185+
Update update = updateMapper.getMappedObject(Update.empty().removeFrom("map").value("foo"), persistentEntity);
186+
187+
assertThat(update.getUpdateOperations()).hasSize(1);
188+
assertThat(update).hasToString("map = map - {'foo'}");
189+
}
190+
182191
@Test // DATACASS-487
183192
void shouldAddUdtToMap() {
184193

0 commit comments

Comments
 (0)