DATACASS-575 - Support IF conditions in lightweight transactions with UPDATE and DELETE.
We now support conditions in lightweight transactions for UPDATE and DELETE statements. Conditions are Filter objects similar to the WHERE clause. Conditions are supported for entity and query-based update/delete methods.
UpdateOptions options = UpdateOptions.builder().ifCondition(where("firstname").is("Walter")).build();
User user = new User("heisenberg", "Walter", "White");
template.update(user, options);
DeleteOptions options = DeleteOptions.builder().ifCondition(where("counter").is(42)).build();
Query query = Query.query(where("id").is("heisenberg")).queryOptions(options);
template.delete(query, User.class);
This commit is contained in:
@@ -312,8 +312,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(query, "Query must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)),
|
||||
entityClass);
|
||||
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)), entityClass);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -325,8 +324,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(query, "Query must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return slice(this.statementFactory.select(query, getRequiredPersistentEntity(entityClass)),
|
||||
entityClass);
|
||||
return slice(this.statementFactory.select(query, getRequiredPersistentEntity(entityClass)), entityClass);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -340,8 +338,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(entityConsumer, "Entity Consumer must not be empty");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)),
|
||||
entityConsumer, entityClass);
|
||||
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)), entityConsumer,
|
||||
entityClass);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -353,8 +351,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(query, "Query must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return selectOne(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)),
|
||||
entityClass);
|
||||
return selectOne(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)), entityClass);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -368,8 +365,8 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(update, "Update must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return getAsyncCqlOperations().execute(
|
||||
getStatementFactory().update(query, update, getRequiredPersistentEntity(entityClass)));
|
||||
return getAsyncCqlOperations()
|
||||
.execute(getStatementFactory().update(query, update, getRequiredPersistentEntity(entityClass)));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -482,7 +479,7 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
|
||||
return new MappingListenableFutureAdapter<>(
|
||||
getAsyncCqlOperations().query(select, (row, rowNum) -> mapper.apply(row)),
|
||||
it -> it.isEmpty() ? null : (T) it.get(0));
|
||||
it -> it.isEmpty() ? null : (T) it.get(0));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -533,8 +530,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "UpdateOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
Update update = EntityQueryUtils.createUpdateQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Update update = getStatementFactory().update(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
maybeEmitEvent(new BeforeSaveEvent<>(entity, tableName, update));
|
||||
|
||||
@@ -562,8 +560,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "QueryOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
Delete delete = EntityQueryUtils.createDeleteQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Delete delete = getStatementFactory().delete(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
maybeEmitEvent(new BeforeDeleteEvent<>(delete, entity.getClass(), tableName));
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ package org.springframework.data.cassandra.core;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.springframework.data.cassandra.core.convert.CassandraConverter;
|
||||
import org.springframework.data.cassandra.core.convert.UpdateMapper;
|
||||
import org.springframework.data.cassandra.core.cql.WriteOptions;
|
||||
import org.springframework.data.cassandra.core.mapping.BasicCassandraPersistentEntity;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.datastax.driver.core.querybuilder.Batch;
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
@@ -42,6 +46,12 @@ class CassandraBatchTemplate implements CassandraBatchOperations {
|
||||
|
||||
private final Batch batch = QueryBuilder.batch();
|
||||
|
||||
private final CassandraConverter converter;
|
||||
|
||||
private final CassandraMappingContext mappingContext;
|
||||
|
||||
private final StatementFactory statementFactory;
|
||||
|
||||
/**
|
||||
* Create a new {@link CassandraBatchTemplate} given {@link CassandraOperations}.
|
||||
*
|
||||
@@ -52,6 +62,9 @@ class CassandraBatchTemplate implements CassandraBatchOperations {
|
||||
Assert.notNull(operations, "CassandraOperations must not be null");
|
||||
|
||||
this.operations = operations;
|
||||
this.converter = operations.getConverter();
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
this.statementFactory = new StatementFactory(new UpdateMapper(converter));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -156,7 +169,10 @@ class CassandraBatchTemplate implements CassandraBatchOperations {
|
||||
for (Object entity : entities) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
batch.add(EntityQueryUtils.createUpdateQuery(getTableName(entity), entity, options, operations.getConverter()));
|
||||
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
batch.add(
|
||||
statementFactory.update(entity, options, this.converter, persistentEntity, persistentEntity.getTableName()));
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -192,8 +208,12 @@ class CassandraBatchTemplate implements CassandraBatchOperations {
|
||||
Assert.notNull(options, "WriteOptions must not be null");
|
||||
|
||||
for (Object entity : entities) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
batch.add(EntityQueryUtils.createDeleteQuery(getTableName(entity), entity, options, operations.getConverter()));
|
||||
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
batch.add(
|
||||
statementFactory.delete(entity, options, this.converter, persistentEntity, persistentEntity.getTableName()));
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -203,10 +223,7 @@ class CassandraBatchTemplate implements CassandraBatchOperations {
|
||||
Assert.state(!executed.get(), "This Cassandra Batch was already executed");
|
||||
}
|
||||
|
||||
private String getTableName(Object entity) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
return operations.getTableName(entity.getClass()).toCql();
|
||||
private CassandraPersistentEntity<?> getRequiredPersistentEntity(Class<?> entityType) {
|
||||
return this.mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(entityType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,8 +589,9 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "UpdateOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
Update update = EntityQueryUtils.createUpdateQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Update update = getStatementFactory().update(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
maybeEmitEvent(new BeforeSaveEvent<>(entity, tableName, update));
|
||||
|
||||
@@ -619,8 +620,9 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "QueryOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
Delete delete = EntityQueryUtils.createDeleteQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Delete delete = getStatementFactory().delete(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
maybeEmitEvent(new BeforeDeleteEvent<>(delete, entity.getClass(), tableName));
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.cassandra.core.cql.WriteOptions;
|
||||
import org.springframework.data.cassandra.core.query.CriteriaDefinition;
|
||||
import org.springframework.data.cassandra.core.query.Filter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.ConsistencyLevel;
|
||||
import com.datastax.driver.core.policies.RetryPolicy;
|
||||
@@ -38,15 +41,18 @@ public class DeleteOptions extends WriteOptions {
|
||||
|
||||
private static final DeleteOptions EMPTY = new DeleteOptionsBuilder().build();
|
||||
|
||||
private boolean ifExists;
|
||||
private final boolean ifExists;
|
||||
|
||||
private final @Nullable Filter ifCondition;
|
||||
|
||||
private DeleteOptions(@Nullable ConsistencyLevel consistencyLevel, @Nullable RetryPolicy retryPolicy,
|
||||
@Nullable Boolean tracing, @Nullable Integer fetchSize, Duration readTimeout, Duration ttl,
|
||||
@Nullable Long timestamp, boolean ifExists) {
|
||||
@Nullable Long timestamp, boolean ifExists, @Nullable Filter ifCondition) {
|
||||
|
||||
super(consistencyLevel, retryPolicy, tracing, fetchSize, readTimeout, ttl, timestamp);
|
||||
|
||||
this.ifExists = ifExists;
|
||||
this.ifCondition = ifCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,12 +84,20 @@ public class DeleteOptions extends WriteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} to apply {@code IF EXISTS} to {@code UPDATE} operations.
|
||||
* @return {@literal true} to apply {@code IF EXISTS} to {@code DELETE} operations.
|
||||
*/
|
||||
public boolean isIfExists() {
|
||||
return this.ifExists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link Filter IF condition} for conditional deletes.
|
||||
*/
|
||||
@Nullable
|
||||
public Filter getIfCondition() {
|
||||
return ifCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link DeleteOptions}.
|
||||
*
|
||||
@@ -93,6 +107,8 @@ public class DeleteOptions extends WriteOptions {
|
||||
|
||||
private boolean ifExists;
|
||||
|
||||
private @Nullable Filter ifCondition;
|
||||
|
||||
private DeleteOptionsBuilder() {}
|
||||
|
||||
private DeleteOptionsBuilder(DeleteOptions deleteOptions) {
|
||||
@@ -223,7 +239,7 @@ public class DeleteOptions extends WriteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}.
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}. Replaces a previous {@link #ifCondition(Filter)}.
|
||||
*
|
||||
* @return {@code this} {@link DeleteOptionsBuilder}
|
||||
*/
|
||||
@@ -232,7 +248,7 @@ public class DeleteOptions extends WriteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}.
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}. Replaces a previous {@link #ifCondition(Filter)}.
|
||||
*
|
||||
* @param ifNotExists {@literal true} to enable {@code IF EXISTS}.
|
||||
* @return {@code this} {@link DeleteOptionsBuilder}
|
||||
@@ -240,6 +256,38 @@ public class DeleteOptions extends WriteOptions {
|
||||
public DeleteOptionsBuilder ifExists(boolean ifNotExists) {
|
||||
|
||||
this.ifExists = ifNotExists;
|
||||
this.ifCondition = null;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF} {@link CriteriaDefinition condition}. Replaces a previous
|
||||
* {@link #ifCondition(Filter)} and {@link #ifExists(boolean)}.
|
||||
*
|
||||
* @param criteria the {@link Filter criteria} to apply for conditional updates, must not be {@literal null}.
|
||||
* @return {@code this} {@link DeleteOptionsBuilder}
|
||||
*/
|
||||
public DeleteOptionsBuilder ifCondition(CriteriaDefinition criteria) {
|
||||
|
||||
Assert.notNull(criteria, "CriteriaDefinition must not be null");
|
||||
|
||||
return ifCondition(Filter.from(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF} {@link Filter condition}. Replaces a previous
|
||||
* {@link #ifCondition(Filter)} and {@link #ifExists(boolean)}.
|
||||
*
|
||||
* @param condition the {@link Filter condition} to apply for conditional deletes, must not be {@literal null}.
|
||||
* @return {@code this} {@link DeleteOptionsBuilder}
|
||||
*/
|
||||
public DeleteOptionsBuilder ifCondition(Filter condition) {
|
||||
|
||||
Assert.notNull(condition, "Filter condition must not be null");
|
||||
|
||||
this.ifCondition = condition;
|
||||
this.ifExists = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -251,7 +299,7 @@ public class DeleteOptions extends WriteOptions {
|
||||
*/
|
||||
public DeleteOptions build() {
|
||||
return new DeleteOptions(this.consistencyLevel, this.retryPolicy, this.tracing, this.fetchSize, this.readTimeout,
|
||||
this.ttl, this.timestamp, this.ifExists);
|
||||
this.ttl, this.timestamp, this.ifExists, this.ifCondition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.cassandra.core.convert.CassandraConverter;
|
||||
import org.springframework.data.cassandra.core.convert.UpdateMapper;
|
||||
import org.springframework.data.cassandra.core.cql.WriteOptions;
|
||||
import org.springframework.data.cassandra.core.mapping.BasicCassandraPersistentEntity;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -55,6 +57,12 @@ class ReactiveCassandraBatchTemplate implements ReactiveCassandraBatchOperations
|
||||
|
||||
private final Batch batch = QueryBuilder.batch();
|
||||
|
||||
private final CassandraConverter converter;
|
||||
|
||||
private final CassandraMappingContext mappingContext;
|
||||
|
||||
private final StatementFactory statementFactory;
|
||||
|
||||
private final List<Mono<Collection<? extends BuiltStatement>>> batchMonos = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
@@ -67,6 +75,9 @@ class ReactiveCassandraBatchTemplate implements ReactiveCassandraBatchOperations
|
||||
Assert.notNull(operations, "CassandraOperations must not be null");
|
||||
|
||||
this.operations = operations;
|
||||
this.converter = operations.getConverter();
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
this.statementFactory = new StatementFactory(new UpdateMapper(converter));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -250,7 +261,9 @@ class ReactiveCassandraBatchTemplate implements ReactiveCassandraBatchOperations
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
updateQueries.add(EntityQueryUtils.createUpdateQuery(getTable(entity), entity, options, converter));
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
updateQueries
|
||||
.add(statementFactory.update(entity, options, converter, persistentEntity, persistentEntity.getTableName()));
|
||||
}
|
||||
|
||||
return updateQueries;
|
||||
@@ -322,7 +335,9 @@ class ReactiveCassandraBatchTemplate implements ReactiveCassandraBatchOperations
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
deleteQueries.add(EntityQueryUtils.createDeleteQuery(getTable(entity), entity, options, converter));
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
deleteQueries
|
||||
.add(statementFactory.delete(entity, options, converter, persistentEntity, persistentEntity.getTableName()));
|
||||
}
|
||||
|
||||
return deleteQueries;
|
||||
@@ -332,11 +347,7 @@ class ReactiveCassandraBatchTemplate implements ReactiveCassandraBatchOperations
|
||||
Assert.state(!executed.get(), "This Cassandra Batch was already executed");
|
||||
}
|
||||
|
||||
private String getTable(Object entity) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
return operations.getConverter().getMappingContext()
|
||||
.getRequiredPersistentEntity(ClassUtils.getUserClass(entity.getClass())).getTableName().toCql();
|
||||
private CassandraPersistentEntity<?> getRequiredPersistentEntity(Class<?> entityType) {
|
||||
return this.mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(entityType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,8 +526,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations, A
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "UpdateOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
Update update = EntityQueryUtils.createUpdateQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Update update = getStatementFactory().update(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
Mono<EntityWriteResult<T>> result = getReactiveCqlOperations() //
|
||||
.execute(new StatementCallback(update)) //
|
||||
@@ -555,9 +556,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations, A
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(options, "QueryOptions must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
|
||||
Delete delete = EntityQueryUtils.createDeleteQuery(tableName.toCql(), entity, options, getConverter());
|
||||
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
CqlIdentifier tableName = persistentEntity.getTableName();
|
||||
Delete delete = getStatementFactory().delete(entity, options, getConverter(), persistentEntity, tableName);
|
||||
|
||||
Mono<WriteResult> result = getReactiveCqlOperations() //
|
||||
.execute(new StatementCallback(delete)) //
|
||||
|
||||
@@ -21,10 +21,13 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.cassandra.core.convert.QueryMapper;
|
||||
import org.springframework.data.cassandra.core.convert.UpdateMapper;
|
||||
import org.springframework.data.cassandra.core.cql.CqlIdentifier;
|
||||
import org.springframework.data.cassandra.core.cql.QueryOptions;
|
||||
import org.springframework.data.cassandra.core.cql.QueryOptionsUtil;
|
||||
import org.springframework.data.cassandra.core.cql.WriteOptions;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
|
||||
@@ -46,6 +49,7 @@ 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.convert.EntityWriter;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
@@ -53,6 +57,7 @@ import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.ProjectionInformation;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -322,6 +327,11 @@ public class StatementFactory {
|
||||
com.datastax.driver.core.querybuilder.Update update = update(tableName, mappedUpdate, filter);
|
||||
|
||||
query.getQueryOptions().ifPresent(queryOptions -> {
|
||||
|
||||
potentiallyApplyIfCondition(queryOptions, UpdateOptions.class, UpdateOptions::getIfCondition, condition -> {
|
||||
addIfCondition(condition, update, entity);
|
||||
});
|
||||
|
||||
if (queryOptions instanceof WriteOptions) {
|
||||
EntityQueryUtils.addWriteOptions(update, (WriteOptions) queryOptions);
|
||||
} else {
|
||||
@@ -334,6 +344,164 @@ public class StatementFactory {
|
||||
return update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@literal UPDATE} statement by mapping {@code entity} to {@link Update} considering
|
||||
* {@link UpdateOptions}.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
* @param entityWriter must not be {@literal null}.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param tableName must not be {@literal null}.
|
||||
* @return the update object.
|
||||
*/
|
||||
com.datastax.driver.core.querybuilder.Update update(Object entity, WriteOptions options,
|
||||
EntityWriter<Object, Object> entityWriter, CassandraPersistentEntity<?> persistentEntity,
|
||||
CqlIdentifier tableName) {
|
||||
|
||||
com.datastax.driver.core.querybuilder.Update update = EntityQueryUtils.createUpdateQuery(tableName.toCql(), entity,
|
||||
options, entityWriter);
|
||||
|
||||
potentiallyApplyIfCondition(options, UpdateOptions.class, UpdateOptions::getIfCondition, condition -> {
|
||||
addIfCondition(condition, update, persistentEntity);
|
||||
});
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, "Entity must not be null");
|
||||
|
||||
return delete(query, entity, entity.getTableName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* @param tableName must not be {@literal null}.
|
||||
* @return the rendered {@link RegularStatement}.
|
||||
* @see 2.1
|
||||
*/
|
||||
public RegularStatement delete(Query query, CassandraPersistentEntity<?> entity, CqlIdentifier tableName) {
|
||||
|
||||
Assert.notNull(query, "Query must not be null");
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(tableName, "Table name must not be null");
|
||||
|
||||
Filter filter = getQueryMapper().getMappedObject(query, entity);
|
||||
|
||||
List<String> columnNames = getQueryMapper().getMappedColumnNames(query.getColumns(), entity);
|
||||
|
||||
Delete delete = delete(columnNames, tableName, filter);
|
||||
|
||||
query.getQueryOptions().ifPresent(queryOptions -> {
|
||||
|
||||
potentiallyApplyIfCondition(queryOptions, DeleteOptions.class, DeleteOptions::getIfCondition, condition -> {
|
||||
addIfCondition(condition, delete, entity);
|
||||
});
|
||||
|
||||
if (queryOptions instanceof WriteOptions) {
|
||||
EntityQueryUtils.addWriteOptions(delete, (WriteOptions) queryOptions);
|
||||
} else {
|
||||
QueryOptionsUtil.addQueryOptions(delete, queryOptions);
|
||||
}
|
||||
});
|
||||
|
||||
query.getPagingState().ifPresent(delete::setPagingState);
|
||||
|
||||
return delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@literal DELETE} statement by mapping {@code entity} to {@link Delete} considering
|
||||
* {@link DeleteOptions}.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
* @param entityWriter must not be {@literal null}.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param tableName must not be {@literal null}.
|
||||
* @return the update object.
|
||||
*/
|
||||
Delete delete(Object entity, QueryOptions options, EntityWriter<Object, Object> entityWriter,
|
||||
CassandraPersistentEntity<?> persistentEntity, CqlIdentifier tableName) {
|
||||
|
||||
Delete delete = EntityQueryUtils.createDeleteQuery(tableName.toCql(), entity, options, entityWriter);
|
||||
|
||||
potentiallyApplyIfCondition(options, DeleteOptions.class, DeleteOptions::getIfCondition, condition -> {
|
||||
addIfCondition(condition, delete, persistentEntity);
|
||||
});
|
||||
|
||||
return delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the {@link Columns} to include type if the {@code returnType} is a {@literal DTO projection} or a
|
||||
* {@literal closed interface projection}.
|
||||
*
|
||||
* @param columns must not be {@literal null}.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param returnType must not be {@literal null}.
|
||||
* @return {@link Columns} with columns to be included.
|
||||
* @since 2.2
|
||||
*/
|
||||
Columns computeColumnsForProjection(Columns columns, PersistentEntity<?, ?> persistentEntity, Class<?> returnType) {
|
||||
|
||||
if (!columns.isEmpty() || ClassUtils.isAssignable(persistentEntity.getType(), returnType)) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
Columns projectedColumns = Columns.empty();
|
||||
|
||||
if (returnType.isInterface()) {
|
||||
|
||||
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
|
||||
|
||||
if (projectionInformation.isClosed()) {
|
||||
|
||||
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
|
||||
projectedColumns = projectedColumns.include(inputProperty.getName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (PersistentProperty<?> property : persistentEntity) {
|
||||
projectedColumns = projectedColumns.include(property.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return projectedColumns;
|
||||
}
|
||||
|
||||
private void addIfCondition(Filter filter, com.datastax.driver.core.querybuilder.Update update,
|
||||
CassandraPersistentEntity<?> persistentEntity) {
|
||||
|
||||
Filter ifCondition = getQueryMapper().getMappedObject(filter, persistentEntity);
|
||||
|
||||
for (CriteriaDefinition criteria : ifCondition) {
|
||||
update.onlyIf(toClause(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
private void addIfCondition(Filter filter, Delete delete, CassandraPersistentEntity<?> persistentEntity) {
|
||||
|
||||
Filter ifCondition = getQueryMapper().getMappedObject(filter, persistentEntity);
|
||||
|
||||
for (CriteriaDefinition criteria : ifCondition) {
|
||||
delete.onlyIf(toClause(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
private static com.datastax.driver.core.querybuilder.Update update(CqlIdentifier table, Update mappedUpdate,
|
||||
Filter filter) {
|
||||
|
||||
@@ -426,92 +594,6 @@ public class StatementFactory {
|
||||
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, "Entity must not be null");
|
||||
|
||||
return delete(query, entity, entity.getTableName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* @param tableName must not be {@literal null}.
|
||||
* @return the rendered {@link RegularStatement}.
|
||||
* @see 2.1
|
||||
*/
|
||||
public RegularStatement delete(Query query, CassandraPersistentEntity<?> entity, CqlIdentifier tableName) {
|
||||
|
||||
Assert.notNull(query, "Query must not be null");
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
Assert.notNull(tableName, "Table name must not be null");
|
||||
|
||||
Filter filter = getQueryMapper().getMappedObject(query, entity);
|
||||
|
||||
List<String> columnNames = getQueryMapper().getMappedColumnNames(query.getColumns(), entity);
|
||||
|
||||
Delete delete = delete(columnNames, tableName, filter);
|
||||
|
||||
query.getQueryOptions().ifPresent(queryOptions -> {
|
||||
if (queryOptions instanceof WriteOptions) {
|
||||
EntityQueryUtils.addWriteOptions(delete, (WriteOptions) queryOptions);
|
||||
} else {
|
||||
QueryOptionsUtil.addQueryOptions(delete, queryOptions);
|
||||
}
|
||||
});
|
||||
|
||||
query.getPagingState().ifPresent(delete::setPagingState);
|
||||
|
||||
return delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the {@link Columns} to include type if the {@code returnType} is a {@literal DTO projection} or a
|
||||
* {@literal closed interface projection}.
|
||||
*
|
||||
* @param columns must not be {@literal null}.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param returnType must not be {@literal null}.
|
||||
* @return {@link Columns} with columns to be included.
|
||||
* @since 2.2
|
||||
*/
|
||||
Columns computeColumnsForProjection(Columns columns, PersistentEntity<?, ?> persistentEntity, Class<?> returnType) {
|
||||
|
||||
if (!columns.isEmpty() || ClassUtils.isAssignable(persistentEntity.getType(), returnType)) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
Columns projectedColumns = Columns.empty();
|
||||
|
||||
if (returnType.isInterface()) {
|
||||
|
||||
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
|
||||
|
||||
if (projectionInformation.isClosed()) {
|
||||
|
||||
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
|
||||
projectedColumns = projectedColumns.include(inputProperty.getName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (PersistentProperty<?> property : persistentEntity) {
|
||||
projectedColumns = projectedColumns.include(property.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return projectedColumns;
|
||||
}
|
||||
|
||||
private static Delete delete(List<String> columnNames, CqlIdentifier from, Filter filter) {
|
||||
|
||||
Delete select;
|
||||
@@ -531,6 +613,25 @@ public class StatementFactory {
|
||||
return select;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a {@link Filter} from an options {@code object} and callback {@link Consumer}. This method checks
|
||||
* defensively if the {@code object} is an instance of {@link Class optionsClass} and tries to extract the
|
||||
* {@link Filter}. If a filter is present, the {@link Consumer} gets called.
|
||||
*/
|
||||
private static <T> void potentiallyApplyIfCondition(@Nullable Object object, Class<T> optionsClass,
|
||||
Function<T, Filter> filterExtractor, Consumer<Filter> consumeIfPresent) {
|
||||
|
||||
if (optionsClass.isInstance(object)) {
|
||||
|
||||
T options = optionsClass.cast(object);
|
||||
Filter filter = filterExtractor.apply(options);
|
||||
if (filter != null) {
|
||||
consumeIfPresent.accept(filter);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Clause toClause(CriteriaDefinition criteriaDefinition) {
|
||||
|
||||
Predicate predicate = criteriaDefinition.getPredicate();
|
||||
@@ -588,4 +689,5 @@ public class StatementFactory {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Criteria %s %s %s not supported", columnName, predicate.getOperator(), predicate.getValue()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.cassandra.core.cql.WriteOptions;
|
||||
import org.springframework.data.cassandra.core.query.CriteriaDefinition;
|
||||
import org.springframework.data.cassandra.core.query.Filter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.ConsistencyLevel;
|
||||
import com.datastax.driver.core.policies.RetryPolicy;
|
||||
@@ -39,15 +42,18 @@ public class UpdateOptions extends WriteOptions {
|
||||
|
||||
private static final UpdateOptions EMPTY = new UpdateOptionsBuilder().build();
|
||||
|
||||
private boolean ifExists;
|
||||
private final boolean ifExists;
|
||||
|
||||
private final @Nullable Filter ifCondition;
|
||||
|
||||
private UpdateOptions(@Nullable ConsistencyLevel consistencyLevel, @Nullable RetryPolicy retryPolicy,
|
||||
@Nullable Boolean tracing, @Nullable Integer fetchSize, Duration readTimeout, Duration ttl,
|
||||
@Nullable Long timestamp, boolean ifExists) {
|
||||
@Nullable Long timestamp, boolean ifExists, @Nullable Filter ifCondition) {
|
||||
|
||||
super(consistencyLevel, retryPolicy, tracing, fetchSize, readTimeout, ttl, timestamp);
|
||||
|
||||
this.ifExists = ifExists;
|
||||
this.ifCondition = ifCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +92,15 @@ public class UpdateOptions extends WriteOptions {
|
||||
return this.ifExists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link Filter IF condition} for conditional updates.
|
||||
* @since 2.2
|
||||
*/
|
||||
@Nullable
|
||||
public Filter getIfCondition() {
|
||||
return ifCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link UpdateOptions}.
|
||||
*
|
||||
@@ -97,6 +112,8 @@ public class UpdateOptions extends WriteOptions {
|
||||
|
||||
private boolean ifExists;
|
||||
|
||||
private @Nullable Filter ifCondition;
|
||||
|
||||
private UpdateOptionsBuilder() {}
|
||||
|
||||
private UpdateOptionsBuilder(UpdateOptions updateOptions) {
|
||||
@@ -104,6 +121,7 @@ public class UpdateOptions extends WriteOptions {
|
||||
super(updateOptions);
|
||||
|
||||
this.ifExists = updateOptions.ifExists;
|
||||
this.ifCondition = updateOptions.ifCondition;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -227,7 +245,7 @@ public class UpdateOptions extends WriteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}.
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}. Replaces a previous {@link #ifCondition(Filter)}.
|
||||
*
|
||||
* @return {@code this} {@link UpdateOptionsBuilder}
|
||||
*/
|
||||
@@ -236,7 +254,7 @@ public class UpdateOptions extends WriteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}.
|
||||
* Use light-weight transactions by applying {@code IF EXISTS}. Replaces a previous {@link #ifCondition(Filter)}.
|
||||
*
|
||||
* @param ifNotExists {@literal true} to enable {@code IF EXISTS}.
|
||||
* @return {@code this} {@link UpdateOptionsBuilder}
|
||||
@@ -244,6 +262,40 @@ public class UpdateOptions extends WriteOptions {
|
||||
public UpdateOptionsBuilder ifExists(boolean ifNotExists) {
|
||||
|
||||
this.ifExists = ifNotExists;
|
||||
this.ifCondition = null;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF} {@link CriteriaDefinition condition}. Replaces a previous
|
||||
* {@link #ifCondition(Filter)} and {@link #ifExists(boolean)}.
|
||||
*
|
||||
* @param criteria the {@link Filter criteria} to apply for conditional updates, must not be {@literal null}.
|
||||
* @return {@code this} {@link UpdateOptionsBuilder}
|
||||
* @since 2.2
|
||||
*/
|
||||
public UpdateOptionsBuilder ifCondition(CriteriaDefinition criteria) {
|
||||
|
||||
Assert.notNull(criteria, "CriteriaDefinition must not be null");
|
||||
|
||||
return ifCondition(Filter.from(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use light-weight transactions by applying {@code IF} {@link Filter condition}. Replaces a previous
|
||||
* {@link #ifCondition(Filter)} and {@link #ifExists(boolean)}.
|
||||
*
|
||||
* @param condition the {@link Filter condition} to apply for conditional updates, must not be {@literal null}.
|
||||
* @return {@code this} {@link UpdateOptionsBuilder}
|
||||
* @since 2.2
|
||||
*/
|
||||
public UpdateOptionsBuilder ifCondition(Filter condition) {
|
||||
|
||||
Assert.notNull(condition, "Filter condition must not be null");
|
||||
|
||||
this.ifCondition = condition;
|
||||
this.ifExists = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -255,7 +307,7 @@ public class UpdateOptions extends WriteOptions {
|
||||
*/
|
||||
public UpdateOptions build() {
|
||||
return new UpdateOptions(this.consistencyLevel, this.retryPolicy, this.tracing, this.fetchSize, this.readTimeout,
|
||||
this.ttl, this.timestamp, this.ifExists);
|
||||
this.ttl, this.timestamp, this.ifExists, this.ifCondition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.cassandra.core.query.Criteria.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -37,7 +36,9 @@ import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.cassandra.CassandraConnectionFailureException;
|
||||
import org.springframework.data.cassandra.core.query.Filter;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
import org.springframework.data.cassandra.core.query.Update;
|
||||
import org.springframework.data.cassandra.domain.User;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -298,6 +299,56 @@ public class AsyncCassandraTemplateUnitTests {
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldUpdateEntityWithOptions() {
|
||||
|
||||
UpdateOptions updateOptions = UpdateOptions.builder().withIfExists().build();
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.update(user, updateOptions);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF EXISTS;");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldUpdateEntityWithLwt() {
|
||||
|
||||
UpdateOptions options = UpdateOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.update(user, options);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQuery() {
|
||||
|
||||
template.update(Query.query(where("id").is("heisenberg")), Update.update("firstname", "Walter"), User.class);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQueryWitLwt() {
|
||||
|
||||
Filter ifCondition = Filter.from(where("firstname").is("Walter"), where("lastname").is("White"));
|
||||
Query query = Query.query(where("id").is("heisenberg"))
|
||||
.queryOptions(UpdateOptions.builder().ifCondition(ifCondition).build());
|
||||
|
||||
template.update(query, Update.update("firstname", "Walter"), User.class);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo(
|
||||
"UPDATE users SET firstname='Walter' WHERE id='heisenberg' IF firstname='Walter' AND lastname='White';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-292
|
||||
public void updateShouldTranslateException() throws Exception {
|
||||
|
||||
@@ -345,6 +396,32 @@ public class AsyncCassandraTemplateUnitTests {
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("DELETE FROM users WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveEntityWithLwt() {
|
||||
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
|
||||
template.delete(user, options);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveByQueryWithLwt() {
|
||||
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
Query query = Query.query(where("id").is("heisenberg")).queryOptions(options);
|
||||
|
||||
template.delete(query, User.class);
|
||||
|
||||
verify(session).executeAsync(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-292
|
||||
public void deleteShouldTranslateException() throws Exception {
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.cassandra.core;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.cassandra.core.query.Criteria.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -32,7 +33,9 @@ import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.cassandra.CassandraConnectionFailureException;
|
||||
import org.springframework.data.cassandra.core.query.Filter;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
import org.springframework.data.cassandra.core.query.Update;
|
||||
import org.springframework.data.cassandra.domain.User;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
@@ -336,6 +339,43 @@ public class CassandraTemplateUnitTests {
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF EXISTS;");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldUpdateEntityWithLwt() {
|
||||
|
||||
UpdateOptions options = UpdateOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.update(user, options);
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQuery() {
|
||||
|
||||
template.update(Query.query(where("id").is("heisenberg")), Update.update("firstname", "Walter"), User.class);
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQueryWitLwt() {
|
||||
|
||||
Filter ifCondition = Filter.from(where("firstname").is("Walter"), where("lastname").is("White"));
|
||||
Query query = Query.query(where("id").is("heisenberg"))
|
||||
.queryOptions(UpdateOptions.builder().ifCondition(ifCondition).build());
|
||||
|
||||
template.update(query, Update.update("firstname", "Walter"), User.class);
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo(
|
||||
"UPDATE users SET firstname='Walter' WHERE id='heisenberg' IF firstname='Walter' AND lastname='White';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-292
|
||||
public void updateShouldTranslateException() {
|
||||
|
||||
@@ -368,8 +408,6 @@ public class CassandraTemplateUnitTests {
|
||||
@Test // DATACASS-292
|
||||
public void deleteShouldRemoveEntity() {
|
||||
|
||||
when(resultSet.wasApplied()).thenReturn(true);
|
||||
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.delete(user);
|
||||
@@ -378,6 +416,32 @@ public class CassandraTemplateUnitTests {
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("DELETE FROM users WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveEntityWithLwt() {
|
||||
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
|
||||
template.delete(user, options);
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveByQueryWithLwt() {
|
||||
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
Query query = Query.query(where("id").is("heisenberg")).queryOptions(options);
|
||||
|
||||
template.delete(query, User.class);
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-292
|
||||
public void deleteShouldTranslateException() {
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2019 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.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DeleteOptions}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class DeleteOptionsUnitTests {
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void shouldConfigureDeleteOptions() {
|
||||
|
||||
Instant now = Instant.ofEpochSecond(1234);
|
||||
|
||||
DeleteOptions deleteOptions = DeleteOptions.builder() //
|
||||
.ttl(10) //
|
||||
.timestamp(now) //
|
||||
.withIfExists() //
|
||||
.build();
|
||||
|
||||
assertThat(deleteOptions.getTtl()).isEqualTo(Duration.ofSeconds(10));
|
||||
assertThat(deleteOptions.getTimestamp()).isEqualTo(now.toEpochMilli() * 1000);
|
||||
assertThat(deleteOptions.isIfExists()).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void buildDeleteOptionsMutate() {
|
||||
|
||||
DeleteOptions deleteOptions = DeleteOptions.builder() //
|
||||
.ttl(10) //
|
||||
.timestamp(1519222753) //
|
||||
.withIfExists() //
|
||||
.build();
|
||||
|
||||
DeleteOptions mutated = deleteOptions.mutate().ttl(20).timestamp(1519000753).build();
|
||||
|
||||
assertThat(mutated).isNotNull();
|
||||
assertThat(mutated).isNotSameAs(deleteOptions);
|
||||
assertThat(mutated.getTtl()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(mutated.getTimestamp()).isEqualTo(1519000753);
|
||||
assertThat(mutated.isIfExists()).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void shouldApplyFilterCondition() {
|
||||
|
||||
DeleteOptions deleteOptions = DeleteOptions.builder() //
|
||||
.withIfExists() //
|
||||
.ifCondition(Query.empty()) //
|
||||
.build();
|
||||
|
||||
assertThat(deleteOptions.isIfExists()).isFalse();
|
||||
assertThat(deleteOptions.getIfCondition()).isEqualTo(Query.empty());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.cassandra.core;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.cassandra.core.query.Criteria.*;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -34,7 +35,9 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.cassandra.ReactiveResultSet;
|
||||
import org.springframework.data.cassandra.ReactiveSession;
|
||||
import org.springframework.data.cassandra.core.query.Filter;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
import org.springframework.data.cassandra.core.query.Update;
|
||||
import org.springframework.data.cassandra.domain.User;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
@@ -261,6 +264,76 @@ public class ReactiveCassandraTemplateUnitTests {
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldUpdateEntityWithOptions() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
UpdateOptions updateOptions = UpdateOptions.builder().withIfExists().build();
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.update(user, updateOptions) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF EXISTS;");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldUpdateEntityWithLwt() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
UpdateOptions options = UpdateOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
|
||||
template.update(user, options) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQuery() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
template.update(Query.query(where("id").is("heisenberg")), Update.update("firstname", "Walter"), User.class) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE users SET firstname='Walter' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void updateShouldApplyUpdateQueryWitLwt() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
Filter ifCondition = Filter.from(where("firstname").is("Walter"), where("lastname").is("White"));
|
||||
Query query = Query.query(where("id").is("heisenberg"))
|
||||
.queryOptions(UpdateOptions.builder().ifCondition(ifCondition).build());
|
||||
|
||||
template.update(query, Update.update("firstname", "Walter"), User.class) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo(
|
||||
"UPDATE users SET firstname='Walter' WHERE id='heisenberg' IF firstname='Walter' AND lastname='White';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-335
|
||||
public void deleteShouldRemoveEntity() {
|
||||
|
||||
@@ -275,6 +348,42 @@ public class ReactiveCassandraTemplateUnitTests {
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("DELETE FROM users WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveEntityWithLwt() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
User user = new User("heisenberg", "Walter", "White");
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
|
||||
template.delete(user, options) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void deleteShouldRemoveByQueryWithLwt() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
|
||||
DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build();
|
||||
Query query = Query.query(where("id").is("heisenberg")).queryOptions(options);
|
||||
|
||||
template.delete(query, User.class) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter';");
|
||||
}
|
||||
|
||||
@Test // DATACASS-335
|
||||
public void truncateShouldRemoveEntities() {
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.cassandra.core.query.Query;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link UpdateOptions}.
|
||||
@@ -35,10 +36,10 @@ public class UpdateOptionsUnitTests {
|
||||
|
||||
Instant now = Instant.ofEpochSecond(1234);
|
||||
|
||||
UpdateOptions updateOptions = UpdateOptions.builder()
|
||||
.ttl(10)
|
||||
.timestamp(now)
|
||||
.withIfExists()
|
||||
UpdateOptions updateOptions = UpdateOptions.builder() //
|
||||
.ttl(10) //
|
||||
.timestamp(now) //
|
||||
.withIfExists() //
|
||||
.build();
|
||||
|
||||
assertThat(updateOptions.getTtl()).isEqualTo(Duration.ofSeconds(10));
|
||||
@@ -49,13 +50,16 @@ public class UpdateOptionsUnitTests {
|
||||
@Test // DATACASS-56, DATACASS-155
|
||||
public void buildUpdateOptionsMutate() {
|
||||
|
||||
UpdateOptions updateOptions = UpdateOptions.builder()
|
||||
.ttl(10)
|
||||
.timestamp(1519222753)
|
||||
.withIfExists()
|
||||
UpdateOptions updateOptions = UpdateOptions.builder() //
|
||||
.ttl(10) //
|
||||
.timestamp(1519222753) //
|
||||
.withIfExists() //
|
||||
.build();
|
||||
|
||||
UpdateOptions mutated = updateOptions.mutate().ttl(20).timestamp(1519000753).build();
|
||||
UpdateOptions mutated = updateOptions.mutate() //
|
||||
.ttl(20) //
|
||||
.timestamp(1519000753) //
|
||||
.build();
|
||||
|
||||
assertThat(mutated).isNotNull();
|
||||
assertThat(mutated).isNotSameAs(updateOptions);
|
||||
@@ -63,4 +67,16 @@ public class UpdateOptionsUnitTests {
|
||||
assertThat(mutated.getTimestamp()).isEqualTo(1519000753);
|
||||
assertThat(mutated.isIfExists()).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACASS-575
|
||||
public void shouldApplyFilterCondition() {
|
||||
|
||||
UpdateOptions updateOptions = UpdateOptions.builder() //
|
||||
.withIfExists() //
|
||||
.ifCondition(Query.empty()) //
|
||||
.build();
|
||||
|
||||
assertThat(updateOptions.isIfExists()).isFalse();
|
||||
assertThat(updateOptions.getIfCondition()).isEqualTo(Query.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ This chapter summarizes changes and new features for each release.
|
||||
* Read-only properties annotated with `@ReadOnlyProperty` to exclude non-writable properties from entity-bound `INSERT` and `UPDATE` operations.
|
||||
* Support for derived `Between` queries.
|
||||
* Kotlin Coroutine extensions for `ReactiveFluentCassandraOperations`.
|
||||
* Lightweight transaction support via `DeleteOptions` using the Template API.
|
||||
* Filter conditions for lightweight transaction update and delete (`UPDATE … IF <condition>`, `DELETE … IF <condition>`).
|
||||
|
||||
[[new-features.2-1-0]]
|
||||
== What's new in Spring Data for Apache Cassandra 2.1
|
||||
|
||||
Reference in New Issue
Block a user