DATACASS-420 - Do not insert null values.

We now no longer insert null values via CassandraOperations.insert(…). Inserting null values creates tombstones in Cassandra which are likely unwanted and impact performance. Omitting null values simply does not create values in Cassandra. Setting properties to null with UPDATE remains unchanged and updating an object containing null values will propagate all null values to Cassandra as part of the UPDATE clause.

SimpleCassandraRepository.save(…) needs to consider whether the entity is new or whether it consists only of primary key properties to propagate the optimization. New entities and entities consisting only of primary key columns are saved via INSERT. All other entities are saved via UPDATE.
This commit is contained in:
Mark Paluch
2017-04-12 13:58:55 +02:00
parent 6c9c65e04d
commit a430e808f0
9 changed files with 319 additions and 23 deletions

View File

@@ -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);
}

View File

@@ -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<T, ID extends Serializable>
extends EntityInformation<T, ID>, CassandraEntityMetadata<T> {
/**
* 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();
}

View File

@@ -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<T, ID extends Serializable> extends AbstractEntityInformation<T, ID>
implements CassandraEntityInformation<T, ID> {
private final CassandraPersistentEntity<T> 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<T, ID extends Serializable> exten
this.entityMetadata = entity;
this.converter = converter;
this.isPrimaryKeyEntity = hasNonIdProperties(entity);
}
/* (non-Javadoc)
@@ -86,4 +91,28 @@ public class MappingCassandraEntityInformation<T, ID extends Serializable> exten
public CqlIdentifier getTableName() {
return entityMetadata.getTableName();
}
@Override
public boolean isPrimaryKeyEntity() {
return isPrimaryKeyEntity;
}
private static boolean hasNonIdProperties(CassandraPersistentEntity<?> entity) {
final AtomicReference<Boolean> hasPrimaryKeyOnlyProperties = new AtomicReference<Boolean>(true);
entity.doWithProperties(new PropertyHandler<CassandraPersistentProperty>() {
@Override
public void doWithPersistentProperty(CassandraPersistentProperty property) {
if (property.isCompositePrimaryKey() || property.isPrimaryKeyColumn() || property.isIdProperty()) {
return;
}
hasPrimaryKeyOnlyProperties.set(false);
}
});
return hasPrimaryKeyOnlyProperties.get();
}
}

View File

@@ -38,9 +38,11 @@ import com.datastax.driver.core.querybuilder.Select;
*/
public class SimpleCassandraRepository<T, ID extends Serializable> implements TypedIdCassandraRepository<T, ID> {
private CassandraEntityInformation<T, ID> entityInformation;
private CassandraOperations operations;
private CassandraEntityInformation<T, ID> entityInformation;
private final boolean isPrimaryKeyEntity;
/**
* Create a new {@link SimpleCassandraRepository} for the given {@link CassandraEntityInformation} and
@@ -56,6 +58,7 @@ public class SimpleCassandraRepository<T, ID extends Serializable> implements Ty
this.entityInformation = metadata;
this.operations = operations;
this.isPrimaryKeyEntity = metadata.isPrimaryKeyEntity();
}
/* (non-Javadoc)
@@ -66,7 +69,11 @@ public class SimpleCassandraRepository<T, ID extends Serializable> 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<T, ID extends Serializable> implements Ty
S saved;
if (entityInformation.isNew(entity)) {
if (entityInformation.isNew(entity) || isPrimaryKeyEntity) {
saved = operations.insert(entity);
} else {
saved = operations.update(entity);

View File

@@ -38,9 +38,12 @@ import com.datastax.driver.core.querybuilder.Select;
public class SimpleReactiveCassandraRepository<T, ID extends Serializable>
implements ReactiveCassandraRepository<T, ID> {
protected ReactiveCassandraOperations operations;
protected CassandraEntityInformation<T, ID> 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<T, ID extends Serializable>
this.entityInformation = metadata;
this.operations = operations;
this.isPrimaryKeyEntity = metadata.isPrimaryKeyEntity();
}
/* (non-Javadoc)
@@ -66,7 +70,7 @@ public class SimpleReactiveCassandraRepository<T, ID extends Serializable>
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<T, ID extends Serializable>
return Flux.from(entityStream).flatMap(entity -> {
if (entityInformation.isNew(entity)) {
if (entityInformation.isNew(entity) || isPrimaryKeyEntity) {
return operations.insert(entity);
}

View File

@@ -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

View File

@@ -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 <a href="https://jira.spring.io/browse/DATACASS-206">DATACASS-206</a>
*/
@Test
@Test // DATACASS-206
public void shouldUseSpecifiedColumnNamesForSingleEntityModifyingOperations() {
UserToken userToken = new UserToken();

View File

@@ -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;
}
}

View File

@@ -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<Object, ? extends Serializable> 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<Object, String>(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<Object, String>(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<Object, String>(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<Object, String>(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;
}
}