From 8d64401aa86a34c3d5fce4737d422c270f596747 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 Jun 2017 14:33:15 +0200 Subject: [PATCH] DATACASS-250 - Support lightweight transactions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support lightweight transactions via InsertOptions and UpdateOptions. Options can be used with the imperative, asynchronous and reactive templates using insert(…) and update(…) write methods. Write methods do not return the entity if the operation is not applied because of a lightweight transaction. Specifically, the resulting entity is null using the imperative and asynchronous Cassandra templates. The reactive Cassandra template suppresses particular entities (inserted/updated through a entity stream) if its write operation was not applied due to a lightweight transaction. InsertOptions lwtInsertOptions = InsertOptions.builder().withIfNotExists().build(); User user = new User("heisenberg", "Walter", "White"); User inserted = template.insert(user, lwtInsertOptions); UpdateOptions lwtUpdateOptions = UpdateOptions.builder().withIfExists().build(); User user = new User("heisenberg", "Walter", "White"); User updated = template.update(user, lwtUpdateOptions); --- .../core/AsyncCassandraOperations.java | 15 +- .../core/AsyncCassandraTemplate.java | 9 +- .../cassandra/core/CassandraOperations.java | 10 +- .../cassandra/core/CassandraTemplate.java | 9 +- .../data/cassandra/core/InsertOptions.java | 172 ++++++++++++++++++ .../data/cassandra/core/QueryUtils.java | 18 ++ .../core/ReactiveCassandraOperations.java | 20 +- .../core/ReactiveCassandraTemplate.java | 17 +- .../data/cassandra/core/UpdateOptions.java | 172 ++++++++++++++++++ .../data/cql/core/QueryOptions.java | 6 +- .../data/cql/core/WriteOptions.java | 12 +- ...syncCassandraTemplateIntegrationTests.java | 80 +++++++- ...assandraBatchTemplateIntegrationTests.java | 24 +++ .../CassandraTemplateIntegrationTests.java | 58 ++++++ .../core/CassandraTemplateUnitTests.java | 34 ++++ .../core/InsertOptionsUnitTests.java | 40 ++++ ...tiveCassandraTemplateIntegrationTests.java | 71 +++++++- .../core/UpdateOptionsUnitTests.java | 40 ++++ src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/cassandra.adoc | 7 +- 20 files changed, 756 insertions(+), 59 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/InsertOptions.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/UpdateOptions.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/InsertOptionsUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/UpdateOptionsUnitTests.java 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 aa6652019..bcc58430b 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 @@ -37,7 +37,10 @@ import com.datastax.driver.core.Statement; * @author John Blum * @since 2.0 * @see AsyncCassandraTemplate - * @see CassandraOperations + * @see AsyncCqlOperations + * @see Statement + * @see InsertOptions + * @see UpdateOptions */ public interface AsyncCassandraOperations { @@ -234,10 +237,11 @@ public interface AsyncCassandraOperations { * * @param entity The entity to insert, must not be {@literal null}. * @param options may be {@literal null}. - * @return the inserted entity. + * @return the inserted entity or a {@literal null} inside of {@link ListenableFuture} if the {@code INSERT} operation + * was not applied. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture insert(T entity, WriteOptions options) throws DataAccessException; + ListenableFuture insert(T entity, InsertOptions options) throws DataAccessException; /** * Update the given entity and return the entity if the update was applied. @@ -253,10 +257,11 @@ public interface AsyncCassandraOperations { * * @param entity The entity to update, must not be {@literal null}. * @param options may be {@literal null}. - * @return the updated entity. + * @return the updated entityor a {@literal null} inside of {@link ListenableFuture} if the {@code UPDATE} operation + * was not applied. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture update(T entity, WriteOptions options) throws DataAccessException; + ListenableFuture update(T entity, UpdateOptions options) throws DataAccessException; /** * Delete the given entity and return the entity if the delete was applied. 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 5542eddd3..e2596998d 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 @@ -36,7 +36,6 @@ import org.springframework.data.cql.core.CqlIdentifier; import org.springframework.data.cql.core.CqlProvider; import org.springframework.data.cql.core.GuavaListenableFutureAdapter; import org.springframework.data.cql.core.QueryOptions; -import org.springframework.data.cql.core.WriteOptions; import org.springframework.data.cql.core.session.DefaultSessionFactory; import org.springframework.data.cql.core.session.SessionFactory; import org.springframework.data.cql.support.CqlExceptionTranslator; @@ -424,10 +423,10 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#insert(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#insert(java.lang.Object, org.springframework.data.cassandra.core.InsertOptions) */ @Override - public ListenableFuture insert(T entity, WriteOptions options) { + public ListenableFuture insert(T entity, InsertOptions options) { Assert.notNull(entity, "Entity must not be null"); @@ -448,10 +447,10 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(java.lang.Object, org.springframework.data.cassandra.core.UpdateOptions) */ @Override - public ListenableFuture update(T entity, WriteOptions options) { + public ListenableFuture update(T entity, UpdateOptions options) { Assert.notNull(entity, "Entity must not be null"); 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 1067d3b0f..07b9e4dbb 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 @@ -41,6 +41,8 @@ import com.datastax.driver.core.Statement; * @see CassandraTemplate * @see CqlOperations * @see Statement + * @see InsertOptions + * @see UpdateOptions */ public interface CassandraOperations { @@ -266,10 +268,10 @@ public interface CassandraOperations { * * @param entity The entity to insert, must not be {@literal null}. * @param options may be {@literal null}. - * @return the inserted entity. + * @return the inserted entity or {@literal null} if the {@code INSERT} operation was not applied. * @throws DataAccessException if there is any problem executing the query. */ - T insert(T entity, WriteOptions options) throws DataAccessException; + T insert(T entity, InsertOptions options) throws DataAccessException; /** * Update the given entity and return the entity if the update was applied. @@ -285,10 +287,10 @@ public interface CassandraOperations { * * @param entity The entity to update, must not be {@literal null}. * @param options may be {@literal null}. - * @return the updated entity. + * @return the updated entity or {@literal null} if the {@code UPDATE} operation was not applied. * @throws DataAccessException if there is any problem executing the query. */ - T update(T entity, WriteOptions options) throws DataAccessException; + T update(T entity, UpdateOptions options) throws DataAccessException; /** * Delete the given entity and return the entity if the delete was applied. 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 2e8aa8330..34e650b5b 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 @@ -37,7 +37,6 @@ import org.springframework.data.cql.core.CqlProvider; import org.springframework.data.cql.core.CqlTemplate; import org.springframework.data.cql.core.QueryOptions; import org.springframework.data.cql.core.SessionCallback; -import org.springframework.data.cql.core.WriteOptions; import org.springframework.data.cql.core.session.DefaultSessionFactory; import org.springframework.data.cql.core.session.SessionFactory; import org.springframework.data.mapping.context.MappingContext; @@ -433,10 +432,10 @@ public class CassandraTemplate implements CassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.CassandraOperations#insert(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.CassandraOperations#insert(java.lang.Object, org.springframework.data.cassandra.core.InsertOptions) */ @Override - public T insert(T entity, WriteOptions options) { + public T insert(T entity, InsertOptions options) { Assert.notNull(entity, "Entity must not be null"); @@ -456,10 +455,10 @@ public class CassandraTemplate implements CassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.CassandraOperations#update(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.CassandraOperations#update(java.lang.Object, org.springframework.data.cassandra.core.UpdateOptions) */ @Override - public T update(T entity, WriteOptions options) { + public T update(T entity, UpdateOptions options) { Assert.notNull(entity, "Entity must not be null"); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/InsertOptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/InsertOptions.java new file mode 100644 index 000000000..82e8a10db --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/InsertOptions.java @@ -0,0 +1,172 @@ +/* + * 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.concurrent.TimeUnit; + +import org.springframework.data.cql.core.WriteOptions; + +/** + * Extension to {@link WriteOptions} for use with {@code INSERT} operations. + * + * @author Mark Paluch + * @since 2.0 + */ +public class InsertOptions extends WriteOptions { + + private boolean ifNotExists; + + /** + * Creates new {@link InsertOptions}. + */ + InsertOptions() {} + + /** + * Create a new {@link InsertOptionsBuilder}. + * + * @return a new {@link InsertOptionsBuilder}. + */ + public static InsertOptionsBuilder builder() { + return new InsertOptionsBuilder(); + } + + /** + * @return {@literal true} to apply {@code IF NOT EXISTS} to {@code INSERT} operations. + */ + public boolean isIfNotExists() { + return ifNotExists; + } + + /** + * Builder for {@link InsertOptions}. + * + * @author Mark Paluch + * @since 2.0 + */ + public static class InsertOptionsBuilder extends WriteOptionsBuilder { + + private boolean ifNotExists; + + private InsertOptionsBuilder() {} + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#consistencyLevel(com.datastax.driver.core.ConsistencyLevel) + */ + @Override + public InsertOptionsBuilder consistencyLevel(com.datastax.driver.core.ConsistencyLevel consistencyLevel) { + return (InsertOptionsBuilder) super.consistencyLevel(consistencyLevel); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#retryPolicy(org.springframework.data.cql.core.RetryPolicy) + */ + @Override + public InsertOptionsBuilder retryPolicy(com.datastax.driver.core.policies.RetryPolicy driverRetryPolicy) { + return (InsertOptionsBuilder) super.retryPolicy(driverRetryPolicy); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#fetchSize(int) + */ + @Override + public InsertOptionsBuilder fetchSize(int fetchSize) { + return (InsertOptionsBuilder) super.fetchSize(fetchSize); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#readTimeout(long) + */ + @Override + public InsertOptionsBuilder readTimeout(long readTimeout) { + return (InsertOptionsBuilder) super.readTimeout(readTimeout); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#readTimeout(long, java.util.concurrent.TimeUnit) + */ + @Override + public InsertOptionsBuilder readTimeout(long readTimeout, TimeUnit timeUnit) { + return (InsertOptionsBuilder) super.readTimeout(readTimeout, timeUnit); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#tracing(boolean) + */ + @Override + public InsertOptionsBuilder tracing(boolean tracing) { + return (InsertOptionsBuilder) super.tracing(tracing); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#withTracing() + */ + @Override + public InsertOptionsBuilder withTracing() { + return (InsertOptionsBuilder) super.withTracing(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.WriteOptions.WriteOptionsBuilder#ttl(int) + */ + public InsertOptionsBuilder ttl(int ttl) { + return (InsertOptionsBuilder) super.ttl(ttl); + } + + /** + * Use light-weight transactions by applying {@code IF NOT EXISTS}. + * + * @return {@code this} {@link InsertOptionsBuilder} + */ + public InsertOptionsBuilder withIfNotExists() { + return ifNotExists(true); + } + + /** + * Use light-weight transactions by applying {@code IF NOT EXISTS}. + * + * @param ifNotExists {@literal true} to enable {@code IF NOT EXISTS}. + * @return {@code this} {@link InsertOptionsBuilder} + */ + public InsertOptionsBuilder ifNotExists(boolean ifNotExists) { + + this.ifNotExists = ifNotExists; + + return this; + } + + /** + * Builds a new {@link InsertOptions} with the configured values. + * + * @return a new {@link InsertOptions} with the configured values + */ + public InsertOptions build() { + + InsertOptions insertOptions = applyOptions(new InsertOptions()); + + insertOptions.ifNotExists = ifNotExists; + + return insertOptions; + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/QueryUtils.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/QueryUtils.java index dea220093..8d514cd45 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/QueryUtils.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/QueryUtils.java @@ -55,6 +55,15 @@ class QueryUtils { Insert insert = QueryOptionsUtil.addWriteOptions(QueryBuilder.insertInto(tableName), options); + if (options instanceof InsertOptions) { + + InsertOptions insertOptions = (InsertOptions) options; + + if (insertOptions.isIfNotExists()) { + insert = insert.ifNotExists(); + } + } + entityWriter.write(objectToUpdate, insert); return insert; @@ -79,6 +88,15 @@ class QueryUtils { Update update = QueryOptionsUtil.addWriteOptions(QueryBuilder.update(tableName), options); + if (options instanceof UpdateOptions) { + + UpdateOptions updateOptions = (UpdateOptions) options; + + if (updateOptions.isIfExists()) { + update.where().ifExists(); + } + } + entityWriter.write(objectToUpdate, update); return update; 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 89bdf9911..0c430cb91 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 @@ -36,6 +36,10 @@ import com.datastax.driver.core.Statement; * @author Mark Paluch * @since 2.0 * @see ReactiveCassandraTemplate + * @see ReactiveCqlOperations + * @see Statement + * @see InsertOptions + * @see UpdateOptions * @see Flux * @see Mono */ @@ -180,10 +184,10 @@ public interface ReactiveCassandraOperations { * * @param entity The entity to insert, must not be {@literal null}. * @param options may be {@literal null}. - * @@return the inserted entity. + * @return the inserted entity or {@link Mono#empty()} if the {@code INSERT} operation was not applied. * @throws DataAccessException if there is any problem issuing the execution. */ - Mono insert(T entity, WriteOptions options) throws DataAccessException; + Mono insert(T entity, InsertOptions options) throws DataAccessException; /** * Insert the given entities and emit the entity if the insert was applied. @@ -199,10 +203,10 @@ public interface ReactiveCassandraOperations { * * @param entities The entities to insert, must not be {@literal null}. * @param options may be {@literal null}. - * @return the inserted entities. + * @return the inserted entities. Does not emit items for which the {@code INSERT} operation was not applied. * @throws DataAccessException if there is any problem issuing the execution. */ - Flux insert(Publisher entities, WriteOptions options) throws DataAccessException; + Flux insert(Publisher entities, InsertOptions options) throws DataAccessException; /** * Update the given entity and emit the entity if the update was applied. @@ -218,10 +222,10 @@ public interface ReactiveCassandraOperations { * * @param entity The entity to update, must not be {@literal null}. * @param options may be {@literal null}. - * @return the updated entity. + * @return the updated entity or {@link Mono#empty()} if the {@code UPDATE} operation was not applied. * @throws DataAccessException if there is any problem issuing the execution. */ - Mono update(T entity, WriteOptions options) throws DataAccessException; + Mono update(T entity, UpdateOptions options) throws DataAccessException; /** * Update the given entities and emit the entity if the update was applied. @@ -237,10 +241,10 @@ public interface ReactiveCassandraOperations { * * @param entities The entities to update. * @param options may be {@literal null}. - * @return the updated entities. + * @return the updated entities. Does not emit items for which the {@code UPDATE} operation was not applied. * @throws DataAccessException if there is any problem issuing the execution. */ - Flux update(Publisher entities, WriteOptions options) throws DataAccessException; + Flux update(Publisher entities, UpdateOptions options) throws DataAccessException; /** * Remove the given object from the table by id. 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 b868db606..8eda48a98 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 @@ -34,7 +34,6 @@ import org.springframework.data.cql.core.QueryOptions; import org.springframework.data.cql.core.ReactiveCqlOperations; import org.springframework.data.cql.core.ReactiveCqlTemplate; import org.springframework.data.cql.core.ReactiveSessionCallback; -import org.springframework.data.cql.core.WriteOptions; import org.springframework.data.cql.core.session.DefaultReactiveSessionFactory; import org.springframework.data.cql.core.session.ReactiveResultSet; import org.springframework.data.cql.core.session.ReactiveSession; @@ -380,10 +379,10 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(java.lang.Object, org.springframework.data.cassandra.core.InsertOptions) */ @Override - public Mono insert(T entity, WriteOptions options) { + public Mono insert(T entity, InsertOptions options) { Assert.notNull(entity, "Entity must not be null"); @@ -417,10 +416,10 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(org.reactivestreams.Publisher, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(org.reactivestreams.Publisher, org.springframework.data.cassandra.core.InsertOptions) */ @Override - public Flux insert(Publisher entities, WriteOptions options) { + public Flux insert(Publisher entities, InsertOptions options) { Assert.notNull(entities, "Entity publisher must not be null"); @@ -438,10 +437,10 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(java.lang.Object, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(java.lang.Object, org.springframework.data.cassandra.core.UpdateOptions) */ @Override - public Mono update(T entity, WriteOptions options) { + public Mono update(T entity, UpdateOptions options) { Assert.notNull(entity, "Entity must not be null"); @@ -475,10 +474,10 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.reactivestreams.Publisher, org.springframework.data.cql.core.WriteOptions) + * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.reactivestreams.Publisher, org.springframework.data.cassandra.core.UpdateOptions) */ @Override - public Flux update(Publisher entities, WriteOptions options) { + public Flux update(Publisher entities, UpdateOptions options) { Assert.notNull(entities, "Entity publisher must not be null"); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/UpdateOptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/UpdateOptions.java new file mode 100644 index 000000000..813777a28 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/UpdateOptions.java @@ -0,0 +1,172 @@ +/* + * 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.concurrent.TimeUnit; + +import org.springframework.data.cql.core.WriteOptions; + +/** + * Extension to {@link WriteOptions} for use with {@code UPDATE} operations. + * + * @author Mark Paluch + * @since 2.0 + */ +public class UpdateOptions extends WriteOptions { + + private boolean ifExists; + + /** + * Creates new {@link UpdateOptions}. + */ + UpdateOptions() {} + + /** + * Create a new {@link UpdateOptionsBuilder}. + * + * @return a new {@link UpdateOptionsBuilder}. + */ + public static UpdateOptionsBuilder builder() { + return new UpdateOptionsBuilder(); + } + + /** + * @return {@literal true} to apply {@code IF EXISTS} to {@code UPDATE} operations. + */ + public boolean isIfExists() { + return ifExists; + } + + /** + * Builder for {@link UpdateOptions}. + * + * @author Mark Paluch + * @since 2.0 + */ + public static class UpdateOptionsBuilder extends WriteOptionsBuilder { + + private boolean ifExists; + + private UpdateOptionsBuilder() {} + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#consistencyLevel(com.datastax.driver.core.ConsistencyLevel) + */ + @Override + public UpdateOptionsBuilder consistencyLevel(com.datastax.driver.core.ConsistencyLevel consistencyLevel) { + return (UpdateOptionsBuilder) super.consistencyLevel(consistencyLevel); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#retryPolicy(org.springframework.data.cql.core.RetryPolicy) + */ + @Override + public UpdateOptionsBuilder retryPolicy(com.datastax.driver.core.policies.RetryPolicy driverRetryPolicy) { + return (UpdateOptionsBuilder) super.retryPolicy(driverRetryPolicy); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#fetchSize(int) + */ + @Override + public UpdateOptionsBuilder fetchSize(int fetchSize) { + return (UpdateOptionsBuilder) super.fetchSize(fetchSize); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#readTimeout(long) + */ + @Override + public UpdateOptionsBuilder readTimeout(long readTimeout) { + return (UpdateOptionsBuilder) super.readTimeout(readTimeout); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#readTimeout(long, java.util.concurrent.TimeUnit) + */ + @Override + public UpdateOptionsBuilder readTimeout(long readTimeout, TimeUnit timeUnit) { + return (UpdateOptionsBuilder) super.readTimeout(readTimeout, timeUnit); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#tracing(boolean) + */ + @Override + public UpdateOptionsBuilder tracing(boolean tracing) { + return (UpdateOptionsBuilder) super.tracing(tracing); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.QueryOptions.QueryOptionsBuilder#withTracing() + */ + @Override + public UpdateOptionsBuilder withTracing() { + return (UpdateOptionsBuilder) super.withTracing(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cql.core.WriteOptions.WriteOptionsBuilder#ttl(int) + */ + public UpdateOptionsBuilder ttl(int ttl) { + return (UpdateOptionsBuilder) super.ttl(ttl); + } + + /** + * Use light-weight transactions by applying {@code IF EXISTS}. + * + * @return {@code this} {@link UpdateOptionsBuilder} + */ + public UpdateOptionsBuilder withIfExists() { + return ifExists(true); + } + + /** + * Use light-weight transactions by applying {@code IF EXISTS}. + * + * @param ifNotExists {@literal true} to enable {@code IF EXISTS}. + * @return {@code this} {@link UpdateOptionsBuilder} + */ + public UpdateOptionsBuilder ifExists(boolean ifNotExists) { + + this.ifExists = ifNotExists; + + return this; + } + + /** + * Builds a new {@link UpdateOptions} with the configured values. + * + * @return a new {@link UpdateOptions} with the configured values + */ + public UpdateOptions build() { + + UpdateOptions insertOptions = applyOptions(new UpdateOptions()); + + insertOptions.ifExists = ifExists; + + return insertOptions; + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/QueryOptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/QueryOptions.java index 7f54c45ce..96848cdf8 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/QueryOptions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/QueryOptions.java @@ -313,7 +313,9 @@ public class QueryOptions { return applyOptions(new QueryOptions()); } - T applyOptions(T queryOptions) { + protected T applyOptions(T options) { + + QueryOptions queryOptions = (QueryOptions) options; queryOptions.setConsistencyLevel(consistencyLevel); queryOptions.setRetryPolicy(retryPolicy); @@ -330,7 +332,7 @@ public class QueryOptions { queryOptions.setTracing(tracing); } - return queryOptions; + return options; } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/WriteOptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/WriteOptions.java index bd562a9a8..13eba4cd2 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/WriteOptions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cql/core/WriteOptions.java @@ -95,7 +95,7 @@ public class WriteOptions extends QueryOptions { private Integer ttl; - private WriteOptionsBuilder() {} + protected WriteOptionsBuilder() {} /* * (non-Javadoc) @@ -177,12 +177,16 @@ public class WriteOptions extends QueryOptions { * @return a new {@link WriteOptions} with the configured values */ public WriteOptions build() { + return applyOptions(new WriteOptions()); + } - WriteOptions queryOptions = applyOptions(new WriteOptions()); + @Override + protected T applyOptions(T queryOptions) { - queryOptions.setTtl(ttl); + WriteOptions writeOptions = (WriteOptions) queryOptions; + writeOptions.setTtl(ttl); - return queryOptions; + return super.applyOptions(queryOptions); } } } 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 29426aee5..87f49d4bf 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 @@ -99,12 +99,41 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea User user = new User("heisenberg", "Walter", "White"); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isNull(); + assertThat(getUser(user.getId())).isNull(); ListenableFuture insert = template.insert(user); assertThat(getUninterruptibly(insert)).isEqualTo(user); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isEqualTo(user); + assertThat(getUser(user.getId())).isEqualTo(user); + } + + @Test // DATACASS-250 + public void insertShouldCreateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture inserted = template.insert(user, lwtOptions); + + assertThat(getUninterruptibly(inserted)).isEqualTo(user); + } + + @Test // DATACASS-250 + public void insertShouldNotUpdateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user, lwtOptions)); + + user.setFirstname("Walter Hartwell"); + + ListenableFuture lwt = template.insert(user, lwtOptions); + + assertThat(getUninterruptibly(lwt)).isNull(); + assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter"); } @Test // DATACASS-292 @@ -125,10 +154,40 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea getUninterruptibly(template.insert(user)); user.setFirstname("Walter Hartwell"); + User updated = getUninterruptibly(template.update(user)); assertThat(updated).isNotNull(); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isEqualTo(user); + assertThat(getUser(user.getId())).isEqualTo(user); + } + + @Test // DATACASS-292 + public void updateShouldNotCreateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture lwt = template.update(user, lwtOptions); + + assertThat(getUninterruptibly(lwt)).isNull(); + assertThat(getUser(user.getId())).isNull(); + } + + @Test // DATACASS-292 + public void updateShouldUpdateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + user.setFirstname("Walter Hartwell"); + + ListenableFuture updated = template.update(user, lwtOptions); + + assertThat(getUninterruptibly(updated)).isNotNull(); + assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter Hartwell"); } @Test // DATACASS-343 @@ -142,8 +201,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea template.update(query, Update.empty().set("firstname", "Walter Hartwell"), User.class)); assertThat(result).isTrue(); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class)).getFirstname()) - .isEqualTo("Walter Hartwell"); + assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter Hartwell"); } @Test // DATACASS-343 @@ -155,7 +213,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea Query query = Query.query(Criteria.where("id").is("heisenberg")); assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isNull(); + assertThat(getUser(user.getId())).isNull(); } @Test // DATACASS-343 @@ -168,7 +226,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); - User loaded = getUninterruptibly(template.selectOneById(user.getId(), User.class)); + User loaded = getUser(user.getId()); assertThat(loaded.getFirstname()).isEqualTo("Walter"); assertThat(loaded.getLastname()).isNull(); } @@ -182,7 +240,7 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea User deleted = getUninterruptibly(template.delete(user)); assertThat(deleted).isNotNull(); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isNull(); + assertThat(getUser(user.getId())).isNull(); } @Test // DATACASS-292 @@ -194,7 +252,11 @@ public class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCrea Boolean deleted = getUninterruptibly(template.deleteById(user.getId(), User.class)); assertThat(deleted).isTrue(); - assertThat(getUninterruptibly(template.selectOneById(user.getId(), User.class))).isNull(); + assertThat(getUser(user.getId())).isNull(); + } + + private User getUser(String id) { + return getUninterruptibly(template.selectOneById(id, User.class)); } private static T getUninterruptibly(Future future) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java index 20a75555e..58cf1d0f2 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.Before; @@ -72,6 +73,29 @@ public class CassandraBatchTemplateIntegrationTests extends AbstractKeyspaceCrea assertThat(loaded.getId().getUsername()).isEqualTo(walter.getId().getUsername()); } + @Test // DATACASS-288 + public void shouldInsertEntitiesWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + Group previousWalter = new Group(new GroupKey("users", "0x1", "walter")); + previousWalter.setAge(42); + template.insert(previousWalter); + + walter.setAge(100); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(template); + batchOperations.insert(Collections.singleton(walter), lwtOptions).insert(mike).execute(); + + Group loadedWalter = template.selectOneById(walter.getId(), Group.class); + Group loadedMike = template.selectOneById(mike.getId(), Group.class); + + assertThat(loadedWalter.getId().getUsername()).isEqualTo(walter.getId().getUsername()); + assertThat(loadedWalter.getAge()).isEqualTo(42); + + assertThat(loadedMike).isNotNull(); + } + @Test // DATACASS-288 public void shouldInsertCollectionOfEntities() { 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 c77cf4ac9..1e17afb09 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 @@ -146,6 +146,35 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(template.selectOneById(user.getId(), User.class)).isEqualTo(user); } + @Test // DATACASS-250 + public void insertShouldCreateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + User inserted = template.insert(user, lwtOptions); + + assertThat(inserted).isEqualTo(user); + } + + @Test // DATACASS-250 + public void insertShouldNotUpdateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + template.insert(user, lwtOptions); + + user.setFirstname("Walter Hartwell"); + + User lwt = template.insert(user, lwtOptions); + + assertThat(lwt).isNull(); + assertThat(template.selectOneById(user.getId(), User.class).getFirstname()).isEqualTo("Walter"); + } + @Test // DATACASS-292 public void shouldInsertAndCountEntities() { @@ -171,6 +200,35 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(template.selectOneById(user.getId(), User.class)).isEqualTo(user); } + @Test // DATACASS-292 + public void updateShouldNotCreateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + User lwt = template.update(user, lwtOptions); + + assertThat(lwt).isNull(); + assertThat(template.selectOneById(user.getId(), User.class)).isNull(); + } + + @Test // DATACASS-292 + public void updateShouldUpdateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + template.insert(user); + + user.setFirstname("Walter Hartwell"); + + User updated = template.update(user, lwtOptions); + + assertThat(updated).isNotNull(); + assertThat(template.selectOneById(user.getId(), User.class).getFirstname()).isEqualTo("Walter Hartwell"); + } + @Test // DATACASS-343 public void updateShouldUpdateEntityByQuery() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java index 94bcd5fa5..797a2f49c 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java @@ -204,6 +204,23 @@ public class CassandraTemplateUnitTests { .isEqualTo("INSERT INTO users (firstname,id,lastname) VALUES ('Walter','heisenberg','White');"); } + @Test // DATACASS-250 + public void insertShouldInsertWithOptionsEntity() { + + InsertOptions insertOptions = InsertOptions.builder().withIfNotExists().build(); + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", "Walter", "White"); + + User inserted = template.insert(user, insertOptions); + + assertThat(inserted).isEqualTo(user); + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()) + .isEqualTo("INSERT INTO users (firstname,id,lastname) VALUES ('Walter','heisenberg','White') IF NOT EXISTS;"); + } + @Test // DATACASS-292 public void insertShouldTranslateException() throws Exception { @@ -246,6 +263,23 @@ public class CassandraTemplateUnitTests { .isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg';"); } + @Test // DATACASS-250 + public void updateShouldUpdateEntityWithOptions() { + + when(resultSet.wasApplied()).thenReturn(true); + + UpdateOptions updateOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + User updated = template.update(user, updateOptions); + + assertThat(updated).isEqualTo(user); + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()) + .isEqualTo("UPDATE users SET firstname='Walter',lastname='White' WHERE id='heisenberg' IF EXISTS;"); + } + @Test // DATACASS-292 public void updateShouldTranslateException() throws Exception { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/InsertOptionsUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/InsertOptionsUnitTests.java new file mode 100644 index 000000000..736909e85 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/InsertOptionsUnitTests.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.Test; + +/** + * Unit tests for {@link InsertOptions}. + * + * @author Mark Paluch + */ +public class InsertOptionsUnitTests { + + @Test // DATACASS-250 + public void shouldConfigureInsertOptions() { + + InsertOptions insertOptions = InsertOptions.builder() // + .ttl(10) // + .withIfNotExists() // + .build(); + + assertThat(insertOptions.getTtl()).isEqualTo(10); + assertThat(insertOptions.isIfNotExists()).isTrue(); + } +} 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 1b874374a..e996606b9 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 @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import reactor.test.StepVerifier.FirstStep; import org.junit.Before; import org.junit.Test; @@ -68,11 +69,39 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC User user = new User("heisenberg", "Walter", "White"); Mono insert = template.insert(user); - StepVerifier.create(template.selectOneById(user.getId(), User.class)).verifyComplete(); + verifyUser(user.getId()).verifyComplete(); StepVerifier.create(insert).expectNext(user).verifyComplete(); - StepVerifier.create(template.selectOneById(user.getId(), User.class)).expectNext(user).verifyComplete(); + verifyUser(user.getId()).expectNext(user).verifyComplete(); + } + + @Test // DATACASS-250 + public void insertShouldCreateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + Mono inserted = template.insert(user, lwtOptions); + + StepVerifier.create(inserted).expectNext(user).verifyComplete(); + } + + @Test // DATACASS-250 + public void insertShouldNotUpdateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + StepVerifier.create(template.insert(user, lwtOptions)).expectNext(user).verifyComplete(); + + user.setFirstname("Walter Hartwell"); + + StepVerifier.create(template.insert(user, lwtOptions)).verifyComplete(); + + verifyUser(user.getId()).consumeNextWith(it -> assertThat(it.getFirstname()).isEqualTo("Walter")).verifyComplete(); } @Test // DATACASS-335 @@ -96,7 +125,35 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.insert(user)).expectNextCount(1).verifyComplete(); - StepVerifier.create(template.selectOneById(user.getId(), User.class)).expectNext(user).verifyComplete(); + verifyUser(user.getId()).expectNext(user).verifyComplete(); + } + + @Test // DATACASS-292 + public void updateShouldNotCreateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + StepVerifier.create(template.update(user, lwtOptions)).verifyComplete(); + + verifyUser(user.getId()).verifyComplete(); + } + + @Test // DATACASS-292 + public void updateShouldUpdateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + StepVerifier.create(template.insert(user)).expectNextCount(1).verifyComplete(); + + user.setFirstname("Walter Hartwell"); + + StepVerifier.create(template.update(user, lwtOptions)).expectNextCount(1).verifyComplete(); + + verifyUser(user.getId()).consumeNextWith(it -> assertThat(it.getFirstname()).isEqualTo("Walter Hartwell")) + .verifyComplete(); } @Test // DATACASS-343 @@ -149,7 +206,7 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.delete(user)).expectNext(user).verifyComplete(); - StepVerifier.create(template.selectOneById(user.getId(), User.class)).verifyComplete(); + verifyUser(user.getId()).verifyComplete(); } @Test // DATACASS-335 @@ -161,7 +218,7 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC StepVerifier.create(template.deleteById(user.getId(), User.class)).expectNext(true).verifyComplete(); - StepVerifier.create(template.selectOneById(user.getId(), User.class)).verifyComplete(); + verifyUser(user.getId()).verifyComplete(); } @Test // DATACASS-343 @@ -199,4 +256,8 @@ public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceC assertThat(template.selectOne(query, UserToken.class).block()).isEqualTo(token1); } + + private FirstStep verifyUser(String userId) { + return StepVerifier.create(template.selectOneById(userId, User.class)); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/UpdateOptionsUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/UpdateOptionsUnitTests.java new file mode 100644 index 000000000..2764585d5 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/UpdateOptionsUnitTests.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.Test; + +/** + * Unit tests for {@link UpdateOptions}. + * + * @author Mark Paluch + */ +public class UpdateOptionsUnitTests { + + @Test // DATACASS-250 + public void shouldConfigureInsertOptions() { + + UpdateOptions insertOptions = UpdateOptions.builder() // + .ttl(10) // + .withIfExists() // + .build(); + + assertThat(insertOptions.getTtl()).isEqualTo(10); + assertThat(insertOptions.isIfExists()).isTrue(); + } +} diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index f684001b9..cd61c94fa 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -5,6 +5,7 @@ == What's new in Spring Data for Apache Cassandra 2.0 * `Update` and `Query` objects. * CRUD repository interface renaming: `CassandraRepository` using `MapId` is now renamed to `MapIdCassandraRepository`. `TypedIdCassandraRepository` is renamed to `CassandraRepository`. +* Lightweight transactions via `InsertOptions` and `UpdateOptions` using the Template API. [[new-features.1-5-0]] == What's new in Spring Data for Apache Cassandra 1.5 diff --git a/src/main/asciidoc/reference/cassandra.adoc b/src/main/asciidoc/reference/cassandra.adoc index 4c5525f95..ce6d13102 100644 --- a/src/main/asciidoc/reference/cassandra.adoc +++ b/src/main/asciidoc/reference/cassandra.adoc @@ -1160,14 +1160,15 @@ Person qp = cassandraTemplate.selectOne(query(where("age").is(33)), Person.class The insert/save operations available to you are listed below. * `T` *insert* `(T objectToSave)` Insert the object in an Apache Cassandra table. -* `T` *insert* `(T objectToSave, WriteOptions writeOptions)` Insert the object in an Apache Cassandra table applying `WriteOptions`. +* `T` *insert* `(T objectToSave, InsertOptions options)` Insert the object in an Apache Cassandra table applying `InsertOptions`. A similar set of update operations is listed below * `T` *update* `(T objectToSave)` Update the object in an Apache Cassandra table. -* `T` *update* `(T objectToSave, WriteOptions writeOptions)` Update the object in an Apache Cassandra table applying `WriteOptions`. +* `T` *update* `(T objectToSave, UpdateOptions options)` Update the object in an Apache Cassandra table applying `UpdateOptions`. -Then, there is always the old fashioned way. You can write your own CQL statements. +Then, there is always the old fashioned way. You can write your own CQL statements. You can configure with `InsertOptions` and `UpdateOptions` +additional options such as TTL, consistency level and lightweight transactions. [source,java] ----