diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java index 6876bb7d9..dd52f406c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java @@ -351,6 +351,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return; } + if (!value.isPresent()) { + return; + } + if (log.isDebugEnabled()) { log.debug("Adding insert.value [{}] - [{}]", property.getColumnName().toCql(), value); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraEntityInformation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraEntityInformation.java index 3f776a745..466dd4f3b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraEntityInformation.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-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. @@ -23,8 +23,18 @@ import org.springframework.data.repository.core.EntityInformation; * Cassandra specific {@link EntityInformation}. * * @author Alex Shvid + * @author Mark Paluch */ public interface CassandraEntityInformation extends EntityInformation, CassandraEntityMetadata { + /** + * Return {@literal true} if the persistent entity consists entirely of primary key properties (a single Id property, + * composite primary key). + * + * @return {@literal true} if the persistent entity consists entirely of primary key properties (a single Id property, + * composite primary key). + * @since 2.0 + */ + boolean isPrimaryKeyEntity(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java index 65e5c66d2..3f0efbe7f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformation.java @@ -17,6 +17,7 @@ package org.springframework.data.cassandra.repository.support; import java.io.Serializable; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.convert.CassandraConverter; @@ -24,6 +25,7 @@ import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.repository.MapId; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.repository.core.support.AbstractEntityInformation; import org.springframework.util.Assert; @@ -34,12 +36,14 @@ import org.springframework.util.Assert; * * @author Alex Shvid * @author Matthew T. Adams + * @author Mark Paluch */ public class MappingCassandraEntityInformation extends AbstractEntityInformation implements CassandraEntityInformation { private final CassandraPersistentEntity entityMetadata; - private CassandraConverter converter; + private final CassandraConverter converter; + private final boolean isPrimaryKeyEntity; /** * Create a new {@link MappingCassandraEntityInformation} for the given {@link CassandraPersistentEntity}. @@ -52,6 +56,7 @@ public class MappingCassandraEntityInformation exten this.entityMetadata = entity; this.converter = converter; + this.isPrimaryKeyEntity = hasNonIdProperties(entity); } /* (non-Javadoc) @@ -86,4 +91,28 @@ public class MappingCassandraEntityInformation exten public CqlIdentifier getTableName() { return entityMetadata.getTableName(); } + + @Override + public boolean isPrimaryKeyEntity() { + return isPrimaryKeyEntity; + } + + private static boolean hasNonIdProperties(CassandraPersistentEntity entity) { + + final AtomicReference hasPrimaryKeyOnlyProperties = new AtomicReference(true); + + entity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(CassandraPersistentProperty property) { + + if (property.isCompositePrimaryKey() || property.isPrimaryKeyColumn() || property.isIdProperty()) { + return; + } + + hasPrimaryKeyOnlyProperties.set(false); + } + }); + + return hasPrimaryKeyOnlyProperties.get(); + } } 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 19a28629a..00a5b1d8b 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 @@ -38,9 +38,11 @@ import com.datastax.driver.core.querybuilder.Select; */ public class SimpleCassandraRepository implements TypedIdCassandraRepository { + private CassandraEntityInformation entityInformation; + private CassandraOperations operations; - private CassandraEntityInformation entityInformation; + private final boolean isPrimaryKeyEntity; /** * Create a new {@link SimpleCassandraRepository} for the given {@link CassandraEntityInformation} and @@ -56,6 +58,7 @@ public class SimpleCassandraRepository implements Ty this.entityInformation = metadata; this.operations = operations; + this.isPrimaryKeyEntity = metadata.isPrimaryKeyEntity(); } /* (non-Javadoc) @@ -66,7 +69,11 @@ public class SimpleCassandraRepository implements Ty Assert.notNull(entity, "Entity must not be null"); - return operations.insert(entity); + if (entityInformation.isNew(entity) || isPrimaryKeyEntity) { + return operations.insert(entity); + } + + return operations.update(entity); } /* (non-Javadoc) @@ -81,7 +88,7 @@ public class SimpleCassandraRepository implements Ty S saved; - if (entityInformation.isNew(entity)) { + if (entityInformation.isNew(entity) || isPrimaryKeyEntity) { saved = operations.insert(entity); } else { saved = operations.update(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 c227b83c9..b30ac16fb 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 @@ -38,9 +38,12 @@ import com.datastax.driver.core.querybuilder.Select; public class SimpleReactiveCassandraRepository implements ReactiveCassandraRepository { - protected ReactiveCassandraOperations operations; protected CassandraEntityInformation entityInformation; + protected ReactiveCassandraOperations operations; + + private final boolean isPrimaryKeyEntity; + /** * Create a new {@link SimpleReactiveCassandraRepository} for the given {@link CassandraEntityInformation} and * {@link ReactiveCassandraOperations}. @@ -56,6 +59,7 @@ public class SimpleReactiveCassandraRepository this.entityInformation = metadata; this.operations = operations; + this.isPrimaryKeyEntity = metadata.isPrimaryKeyEntity(); } /* (non-Javadoc) @@ -66,7 +70,7 @@ public class SimpleReactiveCassandraRepository Assert.notNull(entity, "Entity must not be null"); - if (entityInformation.isNew(entity)) { + if (entityInformation.isNew(entity) || isPrimaryKeyEntity) { return operations.insert(entity); } @@ -95,7 +99,7 @@ public class SimpleReactiveCassandraRepository return Flux.from(entityStream).flatMap(entity -> { - if (entityInformation.isNew(entity)) { + if (entityInformation.isNew(entity) || isPrimaryKeyEntity) { return operations.insert(entity); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUDTIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUDTIntegrationTests.java index c0ae88419..c31c8b65f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUDTIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUDTIntegrationTests.java @@ -156,8 +156,8 @@ public class MappingCassandraConverterUDTIntegrationTests extends AbstractSpring Insert insert = QueryBuilder.insertInto("addressbook"); converter.write(addressBook, insert); - assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " - + "VALUES (null,{zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},'1',null);"); + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (currentaddress,id) " + + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},'1');"); } @Test // DATACASS-172 @@ -193,8 +193,8 @@ public class MappingCassandraConverterUDTIntegrationTests extends AbstractSpring Insert insert = QueryBuilder.insertInto("addressbook"); converter.write(addressBook, insert); - assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " - + "VALUES (null,null,'1',[{zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']}]);"); + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (id,previousaddresses) " + + "VALUES ('1',[{zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']}]);"); } @Test // DATACASS-172 @@ -228,8 +228,8 @@ public class MappingCassandraConverterUDTIntegrationTests extends AbstractSpring Insert insert = QueryBuilder.insertInto("addressbook"); converter.write(addressBook, insert); - assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " - + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},null,'1',null);"); + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,id) " + + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},'1');"); } @Test // DATACASS-172 @@ -302,7 +302,7 @@ public class MappingCassandraConverterUDTIntegrationTests extends AbstractSpring converter.write(bank, insert); assertThat(insert.toString()) - .isEqualTo("INSERT INTO bank (currency,id,othercurrencies) VALUES ({currency:'EUR'},null,null);"); + .isEqualTo("INSERT INTO bank (currency) VALUES ({currency:'EUR'});"); } @Test // DATACASS-172 @@ -361,7 +361,7 @@ public class MappingCassandraConverterUDTIntegrationTests extends AbstractSpring converter.write(bank, insert); assertThat(insert.toString()) - .isEqualTo("INSERT INTO bank (currency,id,othercurrencies) VALUES (null,null,[{currency:'EUR'}]);"); + .isEqualTo("INSERT INTO bank (othercurrencies) VALUES ([{currency:'EUR'}]);"); } @Test // DATACASS-172 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 e604c4ff9..21e8bf2cf 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 @@ -149,8 +149,8 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(loaded.getId()).isEqualTo("heisenberg"); } - @Test // DATACASS-182 - public void insertShouldRemoveFields() { + @Test // DATACASS-182, DATACASS-420 + public void insertShouldNotRemoveFields() { Person person = new Person("heisenberg", "Walter", "White"); @@ -161,7 +161,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI Person loaded = template.selectOneById(person.getId(), Person.class); - assertThat(loaded.getFirstname()).isNull(); + assertThat(loaded.getFirstname()).isEqualTo("Walter"); assertThat(loaded.getId()).isEqualTo("heisenberg"); } @@ -185,10 +185,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(loaded.getBookmarks()).isNull(); } - /** - * @see DATACASS-206 - */ - @Test + @Test // DATACASS-206 public void shouldUseSpecifiedColumnNamesForSingleEntityModifyingOperations() { UserToken userToken = new UserToken(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformationUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformationUnitTests.java new file mode 100644 index 000000000..715947dd7 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformationUnitTests.java @@ -0,0 +1,106 @@ +/* + * 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.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.domain.AllPossibleTypes; +import org.springframework.data.cassandra.domain.CompositeKey; +import org.springframework.data.cassandra.domain.TypeWithKeyClass; +import org.springframework.data.cassandra.domain.TypeWithMapId; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.UserTypeResolver; + +/** + * Unit tests for {@link MappingCassandraEntityInformation}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class MappingCassandraEntityInformationUnitTests { + + BasicCassandraMappingContext context = new BasicCassandraMappingContext(); + + CassandraConverter converter = new MappingCassandraConverter(context); + + @Mock UserTypeResolver userTypeResolver; + + @Before + public void before() { + context.setUserTypeResolver(userTypeResolver); + } + + @Test // DATACASS-420 + public void shouldConsiderSimpleIdEntityAsPrimaryKeyOnly() { + + MappingCassandraEntityInformation information = new MappingCassandraEntityInformation( + context.getRequiredPersistentEntity(PrimaryKeyOnly.class), converter); + + assertThat(information.isPrimaryKeyEntity()).isTrue(); + } + + @Test // DATACASS-420 + public void shouldConsiderCompositeIdEntityAsPrimaryKeyOnly() { + + MappingCassandraEntityInformation information = new MappingCassandraEntityInformation( + context.getRequiredPersistentEntity(CompositeKey.class), converter); + + assertThat(information.isPrimaryKeyEntity()).isTrue(); + } + + @Test // DATACASS-420 + public void shouldConsiderCompositeKeyClassEntityAsPrimaryKeyOnly() { + + MappingCassandraEntityInformation information = new MappingCassandraEntityInformation( + context.getRequiredPersistentEntity(TypeWithKeyClass.class), converter); + + assertThat(information.isPrimaryKeyEntity()).isTrue(); + } + + @Test // DATACASS-420 + public void shouldConsiderMapIdClassEntityAsPrimaryKeyOnly() { + + MappingCassandraEntityInformation information = new MappingCassandraEntityInformation( + context.getRequiredPersistentEntity(TypeWithMapId.class), converter); + + assertThat(information.isPrimaryKeyEntity()).isTrue(); + } + + @Test // DATACASS-420 + public void shouldComplexEntityNotAsPrimaryKeyOnly() { + + MappingCassandraEntityInformation information = new MappingCassandraEntityInformation( + context.getRequiredPersistentEntity(AllPossibleTypes.class), converter); + + assertThat(information.isPrimaryKeyEntity()).isFalse(); + } + + @Data + static class PrimaryKeyOnly { + @Id String id; + } +} 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 new file mode 100644 index 000000000..746779e02 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java @@ -0,0 +1,139 @@ +/* + * 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.repository.support; + +import static org.assertj.core.api.Assertions.*; +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; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.UserTypeResolver; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; + +/** + * Unit tests for {@link SimpleCassandraRepository}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class SimpleCassandraRepositoryUnitTests { + + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + MappingCassandraConverter converter = new MappingCassandraConverter(mappingContext); + + SimpleCassandraRepository repository; + + @Mock CassandraOperations cassandraOperations; + @Mock UserTypeResolver userTypeResolver; + + @Before + public void before() { + mappingContext.setUserTypeResolver(userTypeResolver); + } + + @Test // DATACASS-428 + public void saveShouldInsertNewPrimaryKeyOnlyEntity() { + + CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(SimplePerson.class); + + repository = new SimpleCassandraRepository(new MappingCassandraEntityInformation(entity, converter), + cassandraOperations); + + SimplePerson person = new SimplePerson(); + + when(cassandraOperations.insert(person)).thenReturn(person); + + Object result = repository.save(person); + + assertThat(result).isEqualTo(person); + verify(cassandraOperations).insert(person); + } + + @Test // DATACASS-428 + public void saveShouldUpdateNewEntity() { + + CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); + + repository = new SimpleCassandraRepository(new MappingCassandraEntityInformation(entity, converter), + cassandraOperations); + + Person person = new Person(); + + when(cassandraOperations.update(person)).thenReturn(person); + + Object result = repository.save(person); + + assertThat(result).isEqualTo(person); + verify(cassandraOperations).update(person); + } + + @Test // DATACASS-428 + public void saveShouldUpdateExistingEntity() { + + CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); + + repository = new SimpleCassandraRepository(new MappingCassandraEntityInformation(entity, converter), + cassandraOperations); + + Person person = new Person(); + person.setFirstname("foo"); + person.setLastname("bar"); + + when(cassandraOperations.update(person)).thenReturn(person); + + Object result = repository.save(person); + + assertThat(result).isEqualTo(person); + verify(cassandraOperations).update(person); + } + + @Test // DATACASS-428 + public void insertShouldInsertEntity() { + + CassandraPersistentEntity entity = converter.getMappingContext().getRequiredPersistentEntity(Person.class); + + repository = new SimpleCassandraRepository(new MappingCassandraEntityInformation(entity, converter), + cassandraOperations); + + Person person = new Person(); + + when(cassandraOperations.insert(person)).thenReturn(person); + + Object result = repository.insert(person); + + assertThat(result).isEqualTo(person); + verify(cassandraOperations).insert(person); + } + + @Data + static class SimplePerson { + + @Id String id; + } +}