From df92c8b184237698c0b89a7c8fa79ee01b6f1cb5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 11 Jun 2018 15:14:09 +0200 Subject: [PATCH] DATACASS-560 - Allow INSERT with null values through InsertOptions.insertNulls. We now allow the configuration whether null values of entity objects should be inserted as tombstones using INSERT statements. By default, this option is disable. Enabling tombstones allows upsert usage through INSERT statements as null values get inserted in Cassandra. This option is useful if an object should be persisted in its whole state. Inserting nulls causes tombstones to be created and can have a negative impact on performance. This change allows SimpleRepository and SimpleCassandraRepository to use CassandraTemplate directly allowing emission of lifecycle events. --- .../core/AsyncCassandraTemplate.java | 5 +- .../core/CassandraBatchTemplate.java | 10 ++- .../cassandra/core/CassandraTemplate.java | 4 +- .../data/cassandra/core/InsertOptions.java | 46 +++++++++++++- .../data/cassandra/core/QueryUtils.java | 27 +++++++- .../core/ReactiveCassandraTemplate.java | 9 +-- .../support/SimpleCassandraRepository.java | 17 +++--- .../SimpleReactiveCassandraRepository.java | 12 ++-- .../core/CassandraTemplateUnitTests.java | 28 ++++++--- ...leCassandraRepositoryIntegrationTests.java | 61 ++++++++++++++++++- .../SimpleCassandraRepositoryUnitTests.java | 33 +++------- 11 files changed, 195 insertions(+), 57 deletions(-) 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 3756c2759..9be84554b 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 @@ -502,8 +502,9 @@ public class AsyncCassandraTemplate implements AsyncCassandraOperations, Applica Assert.notNull(entity, "Entity must not be null"); Assert.notNull(options, "InsertOptions must not be null"); - CqlIdentifier tableName = getTableName(entity); - Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + CqlIdentifier tableName = persistentEntity.getTableName(); + Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter(), persistentEntity); maybeEmitEvent(new BeforeSaveEvent<>(entity, tableName, insert)); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java index b352af046..3989f5d94 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java @@ -20,6 +20,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.data.cassandra.core.cql.QueryOptions; 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.util.Assert; import com.datastax.driver.core.querybuilder.Batch; @@ -109,10 +111,16 @@ class CassandraBatchTemplate implements CassandraBatchOperations { Assert.notNull(entities, "Entities must not be null"); Assert.notNull(options, "WriteOptions must not be null"); + CassandraMappingContext mappingContext = operations.getConverter().getMappingContext(); + for (Object entity : entities) { Assert.notNull(entity, "Entity must not be null"); - batch.add(QueryUtils.createInsertQuery(getTableName(entity), entity, options, operations.getConverter())); + + BasicCassandraPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(entity.getClass()); + batch.add(QueryUtils.createInsertQuery(persistentEntity.getTableName().toCql(), entity, options, + operations.getConverter(), persistentEntity)); } return this; 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 373c7291d..214c6daf9 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 @@ -549,7 +549,9 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP WriteResult doInsert(Object entity, WriteOptions options, CqlIdentifier tableName) { - Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + + Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter(), persistentEntity); maybeEmitEvent(new BeforeSaveEvent<>(entity, tableName, insert)); 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 index e6ac46588..e27a9e7d7 100644 --- 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 @@ -41,13 +41,16 @@ public class InsertOptions extends WriteOptions { private boolean ifNotExists; + private boolean insertNulls; + private InsertOptions(@Nullable ConsistencyLevel consistencyLevel, @Nullable RetryPolicy retryPolicy, @Nullable Boolean tracing, @Nullable Integer fetchSize, Duration readTimeout, Duration ttl, - @Nullable Long timestamp, boolean ifNotExists) { + @Nullable Long timestamp, boolean ifNotExists, boolean insertNulls) { super(consistencyLevel, retryPolicy, tracing, fetchSize, readTimeout, ttl, timestamp); this.ifNotExists = ifNotExists; + this.insertNulls = insertNulls; } /** @@ -86,6 +89,14 @@ public class InsertOptions extends WriteOptions { return this.ifNotExists; } + /** + * @return {@literal true} to insert {@literal null} values from an entity. + * @since 2.1 + */ + public boolean isInsertNulls() { + return this.insertNulls; + } + /** * Builder for {@link InsertOptions}. * @@ -97,6 +108,8 @@ public class InsertOptions extends WriteOptions { private boolean ifNotExists; + private boolean insertNulls; + private InsertOptionsBuilder() {} private InsertOptionsBuilder(InsertOptions insertOptions) { @@ -104,6 +117,7 @@ public class InsertOptions extends WriteOptions { super(insertOptions); this.ifNotExists = insertOptions.ifNotExists; + this.insertNulls = insertOptions.insertNulls; } /* (non-Javadoc) @@ -247,6 +261,34 @@ public class InsertOptions extends WriteOptions { return this; } + /** + * Insert {@literal null} values from an entity. This allows the usage of {@code INSERT} statements as upsert by + * ensuring * that the whole entity state is persisted. Inserting {@literal null}s in Cassandra creates tombstones + * so this * option should be used with caution. + * + * @return {@code this} {@link InsertOptionsBuilder} + * @since 2.1 + */ + public InsertOptionsBuilder withInsertNulls() { + return withInsertNulls(true); + } + + /** + * Insert {@literal null} values from an entity. This allows the usage of {@code INSERT} statements as upsert by + * ensuring that the whole entity state is persisted. Inserting {@literal null}s in Cassandra creates tombstones so + * this option should be used with caution. + * + * @param insertNulls {@literal true} to enable insertion of {@literal null} values. + * @return {@code this} {@link InsertOptionsBuilder} + * @since 2.1 + */ + public InsertOptionsBuilder withInsertNulls(boolean insertNulls) { + + this.insertNulls = insertNulls; + + return this; + } + /** * Builds a new {@link InsertOptions} with the configured values. * @@ -254,7 +296,7 @@ public class InsertOptions extends WriteOptions { */ public InsertOptions build() { return new InsertOptions(this.consistencyLevel, this.retryPolicy, this.tracing, this.fetchSize, this.readTimeout, - this.ttl, this.timestamp, this.ifNotExists); + this.ttl, this.timestamp, this.ifNotExists, this.insertNulls); } } } 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 16c3e79f8..f84332881 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 @@ -16,16 +16,21 @@ package org.springframework.data.cassandra.core; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.data.cassandra.core.convert.CassandraConverter; 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.RowMapper; import org.springframework.data.cassandra.core.cql.WriteOptions; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.query.CassandraPageRequest; import org.springframework.data.convert.EntityWriter; import org.springframework.data.domain.PageRequest; @@ -64,17 +69,20 @@ class QueryUtils { * @param objectToUpdate the object to save, must not be {@literal null}. * @param options optional {@link WriteOptions} to apply to the {@link Insert} statement, may be {@literal null}. * @param entityWriter the {@link EntityWriter} to write insert values. + * @param entity must not be {@literal null}. * @return The Query object to run with session.execute(); */ static Insert createInsertQuery(String tableName, Object objectToUpdate, WriteOptions options, - EntityWriter entityWriter) { + CassandraConverter entityWriter, CassandraPersistentEntity entity) { Assert.hasText(tableName, "TableName must not be empty"); Assert.notNull(objectToUpdate, "Object to insert must not be null"); - Assert.notNull(entityWriter, "EntityWriter must not be null"); + Assert.notNull(entityWriter, "CassandraConverter must not be null"); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); Insert insert = QueryOptionsUtil.addWriteOptions(QueryBuilder.insertInto(tableName), options); + boolean insertNulls = false; if (options instanceof InsertOptions) { InsertOptions insertOptions = (InsertOptions) options; @@ -82,9 +90,22 @@ class QueryUtils { if (insertOptions.isIfNotExists()) { insert = insert.ifNotExists(); } + + insertNulls = insertOptions.isInsertNulls(); } - entityWriter.write(objectToUpdate, insert); + if (insertNulls) { + + Map toInsert = new LinkedHashMap<>(); + + entityWriter.write(objectToUpdate, toInsert, entity); + + for (Entry entry : toInsert.entrySet()) { + insert.value(entry.getKey(), entry.getValue()); + } + } else { + entityWriter.write(objectToUpdate, insert); + } return insert; } 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 f427412ba..cf0440040 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 @@ -15,13 +15,12 @@ */ package org.springframework.data.cassandra.core; -import java.util.function.Function; - import lombok.Value; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.function.Function; + import org.reactivestreams.Publisher; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -433,7 +432,9 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations, A Mono doInsert(Object entity, WriteOptions options, CqlIdentifier tableName) { - Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + + Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter(), persistentEntity); // noinspection ConstantConditions Mono result = getReactiveCqlOperations().execute(new StatementCallback(insert)) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepository.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepository.java index 0d939da0e..fda5baff5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepository.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepository.java @@ -15,7 +15,7 @@ */ package org.springframework.data.cassandra.repository.support; -import static org.springframework.data.cassandra.core.query.Criteria.where; +import static org.springframework.data.cassandra.core.query.Criteria.*; import java.util.ArrayList; import java.util.List; @@ -23,6 +23,7 @@ import java.util.Optional; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.InsertOptions; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; @@ -46,13 +47,15 @@ import com.datastax.driver.core.querybuilder.Select; */ public class SimpleCassandraRepository implements CassandraRepository { + private static final InsertOptions INSERT_NULLS = InsertOptions.builder().withInsertNulls().build(); + private final CassandraEntityInformation entityInformation; private final CassandraOperations operations; /** - * Create a new {@link SimpleCassandraRepository} for the given {@link CassandraEntityInformation} - * and {@link CassandraTemplate}. + * Create a new {@link SimpleCassandraRepository} for the given {@link CassandraEntityInformation} and + * {@link CassandraTemplate}. * * @param metadata must not be {@literal null}. * @param operations must not be {@literal null}. @@ -74,9 +77,7 @@ public class SimpleCassandraRepository implements CassandraRepository implements CassandraRepository implements CassandraRepository Insert createInsert(S entity) { return InsertUtil.createInsert(operations.getConverter(), entity); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepository.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepository.java index 243e6f909..2471e22ba 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepository.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/SimpleReactiveCassandraRepository.java @@ -18,13 +18,13 @@ package org.springframework.data.cassandra.repository.support; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.reactivestreams.Publisher; +import org.springframework.data.cassandra.core.InsertOptions; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.repository.ReactiveCassandraRepository; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; import org.springframework.util.Assert; -import org.reactivestreams.Publisher; - import com.datastax.driver.core.querybuilder.Insert; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; @@ -38,6 +38,8 @@ import com.datastax.driver.core.querybuilder.Select; */ public class SimpleReactiveCassandraRepository implements ReactiveCassandraRepository { + private static final InsertOptions INSERT_NULLS = InsertOptions.builder().withInsertNulls().build(); + private final CassandraEntityInformation entityInformation; private final ReactiveCassandraOperations operations; @@ -67,7 +69,7 @@ public class SimpleReactiveCassandraRepository implements ReactiveCassand Assert.notNull(entity, "Entity must not be null"); - return operations.getReactiveCqlOperations().execute(createInsert(entity)).map(it -> entity); + return operations.insert(entity, INSERT_NULLS).thenReturn(entity); } /** @@ -75,6 +77,8 @@ public class SimpleReactiveCassandraRepository implements ReactiveCassand * * @param entity the entity, must not be {@literal null}. * @return the constructed {@link Insert} statement. + * @deprecated since 2.1, use {@link InsertOptions#isInsertNulls()} with + * {@link ReactiveCassandraOperations#insert(Object, InsertOptions)}. */ private Insert createInsert(S entity) { return InsertUtil.createInsert(operations.getConverter(), entity); @@ -100,7 +104,7 @@ public class SimpleReactiveCassandraRepository implements ReactiveCassand Assert.notNull(entityStream, "The given Publisher of entities must not be null"); return Flux.from(entityStream) - .flatMap(entity -> operations.getReactiveCqlOperations().execute(createInsert(entity)).map(it -> entity)); + .flatMap(entity -> operations.insert(entity, INSERT_NULLS).thenReturn(entity)); } /* (non-Javadoc) 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 716ccdfb2..fa02dcdf4 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 @@ -15,14 +15,11 @@ */ package org.springframework.data.cassandra.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +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.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.Collections; import java.util.List; @@ -36,7 +33,6 @@ import org.mockito.Captor; 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.Query; import org.springframework.data.cassandra.domain.User; @@ -250,8 +246,24 @@ public class CassandraTemplateUnitTests { .isEqualTo("INSERT INTO users (firstname,id,lastname) VALUES ('Walter','heisenberg','White') IF NOT EXISTS;"); } + @Test // DATACASS-560 + public void insertShouldInsertWithNulls() { + + InsertOptions insertOptions = InsertOptions.builder().withInsertNulls().build(); + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", null, null); + + template.insert(user, insertOptions); + + verify(session).execute(statementCaptor.capture()); + assertThat(statementCaptor.getValue().toString()) + .isEqualTo("INSERT INTO users (firstname,id,lastname) VALUES (null,'heisenberg',null);"); + } + @Test // DATACASS-292 - public void insertShouldTranslateException() throws Exception { + public void insertShouldTranslateException() { reset(session); when(session.execute(any(Statement.class))).thenThrow(new NoHostAvailableException(Collections.emptyMap())); @@ -351,7 +363,7 @@ public class CassandraTemplateUnitTests { } @Test // DATACASS-292 - public void deleteShouldTranslateException() throws Exception { + public void deleteShouldTranslateException() { reset(session); when(session.execute(any(Statement.class))).thenThrow(new NoHostAvailableException(Collections.emptyMap())); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryIntegrationTests.java index f78bf6194..d6ba02b97 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryIntegrationTests.java @@ -15,23 +15,29 @@ */ package org.springframework.data.cassandra.repository.support; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.mapping.event.AbstractCassandraEventListener; +import org.springframework.data.cassandra.core.mapping.event.AfterSaveEvent; +import org.springframework.data.cassandra.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.cassandra.core.mapping.event.CassandraMappingEvent; import org.springframework.data.cassandra.core.query.CassandraPageRequest; import org.springframework.data.cassandra.domain.User; import org.springframework.data.cassandra.repository.CassandraRepository; @@ -59,9 +65,15 @@ public class SimpleCassandraRepositoryIntegrationTests extends AbstractKeyspaceC public String[] getEntityBasePackages() { return new String[] { User.class.getPackage().getName() }; } + + @Bean + CaptureEventListener eventListener() { + return new CaptureEventListener(); + } } @Autowired private CassandraOperations operations; + @Autowired private CaptureEventListener eventListener; private BeanFactory beanFactory; private CassandraRepositoryFactory factory; @@ -99,6 +111,8 @@ public class SimpleCassandraRepositoryIntegrationTests extends AbstractKeyspaceC boyd = new User("45", "Boyd", "Tinsley"); repository.saveAll(Arrays.asList(oliver, dave, carter, boyd)); + + eventListener.clear(); } @Test // DATACASS-396 @@ -217,6 +231,18 @@ public class SimpleCassandraRepositoryIntegrationTests extends AbstractKeyspaceC }); } + @Test // DATACASS-560 + public void saveShouldEmitEvents() { + + dave.setFirstname("Hello, Dave"); + dave.setLastname("Bowman"); + + repository.save(dave); + + assertThat(eventListener.getBeforeSave()).hasSize(1); + assertThat(eventListener.getAfterSave()).hasSize(1); + } + @Test // DATACASS-396 public void saveEntityShouldInsertNewEntity() { @@ -304,4 +330,35 @@ public class SimpleCassandraRepositoryIntegrationTests extends AbstractKeyspaceC interface UserRepostitory extends CassandraRepository { } + static class CaptureEventListener extends AbstractCassandraEventListener { + + private final List> events = new CopyOnWriteArrayList<>(); + + @Override + public void onBeforeSave(BeforeSaveEvent event) { + events.add(event); + } + + @Override + public void onAfterSave(AfterSaveEvent event) { + events.add(event); + } + + private void clear() { + events.clear(); + } + + List> getBeforeSave() { + return filter(BeforeSaveEvent.class); + } + + List> getAfterSave() { + return filter(AfterSaveEvent.class); + } + + @SuppressWarnings("unchecked") + private List filter(Class targetType) { + return (List) events.stream().filter(targetType::isInstance).map(targetType::cast).collect(Collectors.toList()); + } + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java index f960128ae..d47cf856e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java @@ -15,15 +15,12 @@ */ package org.springframework.data.cassandra.repository.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.Serializable; +import static org.mockito.Mockito.*; import lombok.Data; +import java.io.Serializable; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,11 +28,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.InsertOptions; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; -import org.springframework.data.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.core.cql.CqlOperations; import org.springframework.data.cassandra.core.cql.QueryOptions; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; @@ -72,15 +68,10 @@ public class SimpleCassandraRepositoryUnitTests { @Before public void before() { - mappingContext.setUserTypeResolver(userTypeResolver); - - when(cassandraOperations.getConverter()).thenReturn(converter); - when(cassandraOperations.getCqlOperations()).thenReturn(cqlOperations); - when(userTypeResolver.resolveType(CqlIdentifier.of("address"))).thenReturn(userType); } - @Test // DATACASS-428 + @Test // DATACASS-428, DATACASS-560 public void saveShouldInsertNewPrimaryKeyOnlyEntity() { CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(SimplePerson.class); @@ -92,11 +83,10 @@ public class SimpleCassandraRepositoryUnitTests { repository.save(person); - verify(cqlOperations).execute(insertCaptor.capture()); - assertThat(insertCaptor.getValue().toString()).isEqualTo("INSERT INTO simpleperson (id) VALUES (null);"); + verify(cassandraOperations).insert(person, InsertOptions.builder().withInsertNulls().build()); } - @Test // DATACASS-428 + @Test // DATACASS-428, DATACASS-560 public void saveShouldUpdateNewEntity() { CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); @@ -108,10 +98,10 @@ public class SimpleCassandraRepositoryUnitTests { repository.save(person); - verify(cqlOperations).execute(any(Insert.class)); + verify(cassandraOperations).insert(person, InsertOptions.builder().withInsertNulls().build()); } - @Test // DATACASS-428 + @Test // DATACASS-428, DATACASS-560 public void saveShouldUpdateExistingEntity() { CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); @@ -125,10 +115,7 @@ public class SimpleCassandraRepositoryUnitTests { repository.save(person); - verify(cqlOperations).execute(insertCaptor.capture()); - assertThat(insertCaptor.getValue().toString()) - .contains("INSERT INTO person (lastname,firstname,alternativeaddresses,"); - assertThat(insertCaptor.getValue().toString()).contains("VALUES ('bar','foo',null"); + verify(cassandraOperations).insert(person, InsertOptions.builder().withInsertNulls().build()); } @Test // DATACASS-428