From 3a062afafbe02efe5074e377e63c9ed3ee77b700 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 31 Jan 2019 10:01:01 +0100 Subject: [PATCH] DATACASS-623 - Add support for read-only properties. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support read-only properties by annotating such properties with @ReadOnlyProperty. Read-only properties are read from results but never written (i.e. through INSERT or UPDATE) back to Cassandra. Read-only properties can still be updated by using the Update/Query API. class Person { @Id String id; String firstname; String lastname; @ReadOnlyProperty String externallyUpdatedProperty; } CassandraTemplate template = … Person person = … template.insert(person); --- .../convert/MappingCassandraConverter.java | 12 +++++++ ...MappingCassandraConverterUDTUnitTests.java | 35 +++++++++++-------- .../MappingCassandraConverterUnitTests.java | 30 ++++++++++++++++ src/main/asciidoc/new-features.adoc | 4 +++ src/main/asciidoc/reference/mapping.adoc | 1 + 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java index c1f551d0b..85929a7a1 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java @@ -435,6 +435,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter for (CassandraPersistentProperty property : entity) { + if (!property.isWritable()) { + continue; + } + Object value = getWriteValue(property, propertyAccessor); if (log.isDebugEnabled()) { @@ -475,6 +479,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter for (CassandraPersistentProperty property : entity) { + if (!property.isWritable()) { + continue; + } + Object value = getWriteValue(property, propertyAccessor); if (property.isCompositePrimaryKey()) { @@ -643,6 +651,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter for (CassandraPersistentProperty property : entity) { + if (!property.isWritable()) { + continue; + } + Object value = getWriteValue(property, propertyAccessor); if (log.isDebugEnabled()) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java index f0e914579..307d6c6d7 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java @@ -15,9 +15,12 @@ */ package org.springframework.data.cassandra.core.convert; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; -import static org.springframework.data.cassandra.test.util.RowMockUtil.column; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.cassandra.test.util.RowMockUtil.*; + +import lombok.AllArgsConstructor; +import lombok.Data; import java.util.Arrays; import java.util.Collections; @@ -25,9 +28,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import lombok.AllArgsConstructor; -import lombok.Data; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -35,7 +35,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - +import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.UserDefinedType; @@ -61,7 +61,8 @@ public class MappingCassandraConverterUDTUnitTests { @Rule public final ExpectedException expectedException = ExpectedException.none(); @Mock UserTypeResolver userTypeResolver; - UserType manufacturer = UserTypeBuilder.forName("manufacturer").withField("name", DataType.varchar()).build(); + UserType manufacturer = UserTypeBuilder.forName("manufacturer").withField("name", DataType.varchar()) + .withField("displayname", DataType.varchar()).build(); UserType currency = UserTypeBuilder.forName("mycurrency").withField("currency", DataType.varchar()).build(); Row rowMock; @@ -82,10 +83,10 @@ public class MappingCassandraConverterUDTUnitTests { when(userTypeResolver.resolveType(CqlIdentifier.of("currency"))).thenReturn(currency); } - @Test // DATACASS-487 + @Test // DATACASS-487, DATACASS-623 public void shouldReadMappedUdtInMap() { - UDTValue key = manufacturer.newValue().setString("name", "a good one"); + UDTValue key = manufacturer.newValue().setString("name", "a good one").setString("displayname", "my displayName"); UDTValue value1 = currency.newValue().setString("currency", "EUR"); UDTValue value2 = currency.newValue().setString("currency", "USD"); @@ -93,21 +94,22 @@ public class MappingCassandraConverterUDTUnitTests { map.put(key, Arrays.asList(value1, value2)); - rowMock = RowMockUtil.newRowMock(column("acceptedCurrencies", map, DataType.map(manufacturer, DataType.list(currency)))); + rowMock = RowMockUtil + .newRowMock(column("acceptedCurrencies", map, DataType.map(manufacturer, DataType.list(currency)))); Supplier supplier = mappingCassandraConverter.read(Supplier.class, rowMock); assertThat(supplier.getAcceptedCurrencies()).isNotEmpty(); - List currencies = supplier.getAcceptedCurrencies().get(new Manufacturer("a good one")); + List currencies = supplier.getAcceptedCurrencies().get(new Manufacturer("a good one", "my displayName")); assertThat(currencies).contains(new Currency("EUR"), new Currency("USD")); } - @Test // DATACASS-487 + @Test // DATACASS-487, DATACASS-623 public void shouldWriteMappedUdtInMap() { - Map> currencies = Collections.singletonMap(new Manufacturer("a good one"), + Map> currencies = Collections.singletonMap(new Manufacturer("a good one", "foo"), Arrays.asList(new Currency("EUR"), new Currency("USD"))); Supplier supplier = new Supplier(currencies); @@ -116,14 +118,17 @@ public class MappingCassandraConverterUDTUnitTests { mappingCassandraConverter.write(supplier, insert); - assertThat(insert.toString()).contains("VALUES ({{name:'a good one'}:[{currency:'EUR'},{currency:'USD'}]}"); + assertThat(insert.toString()) + .contains("VALUES ({{name:'a good one',displayname:NULL}:[{currency:'EUR'},{currency:'USD'}]}"); } @UserDefinedType @Data @AllArgsConstructor private static class Manufacturer { + String name; + @ReadOnlyProperty String displayName; } @UserDefinedType diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java index ed52603f7..1f22ee13f 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java @@ -42,6 +42,7 @@ import org.junit.rules.ExpectedException; import org.springframework.core.SpringVersion; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Transient; import org.springframework.data.cassandra.core.cql.PrimaryKeyType; import org.springframework.data.cassandra.core.mapping.BasicMapId; @@ -964,6 +965,34 @@ public class MappingCassandraConverterUnitTests { assertThat(insert.toString()).isEqualTo("INSERT INTO table (firstname,lastname) VALUES ('Foo','Bar');"); } + @Test // DATACASS-623 + public void insertShouldSkipTransientReadProperties() { + + WithTransient withTransient = new WithTransient(); + withTransient.firstname = "Foo"; + withTransient.computedName = "FooBar"; + + Insert insert = QueryBuilder.insertInto("table"); + + this.mappingCassandraConverter.write(withTransient, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO table (firstname) VALUES ('Foo');"); + } + + @Test // DATACASS-623 + public void updateShouldSkipTransientReadProperties() { + + WithTransient withTransient = new WithTransient(); + withTransient.firstname = "Foo"; + withTransient.computedName = "FooBar"; + + Update update = QueryBuilder.update("table"); + + this.mappingCassandraConverter.write(withTransient, update); + + assertThat(update.toString()).isEqualTo("UPDATE table SET firstname='Foo',lastname=null WHERE id=null;"); + } + @SuppressWarnings("unchecked") private static List getListValue(Insert statement) { @@ -1233,5 +1262,6 @@ public class MappingCassandraConverterUnitTests { String firstname; String lastname; @Transient String displayName; + @ReadOnlyProperty String computedName; } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index bc0f464f0..1053550ee 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -3,6 +3,10 @@ This chapter summarizes changes and new features for each release. +[[new-features.2-2-0]] +== What's new in Spring Data for Apache Cassandra 2.2 +* Read-only properties annotated with `@ReadOnlyProperty` to exclude non-writable properties from entity-bound `INSERT` and `UPDATE` operations. + [[new-features.2-1-0]] == What's new in Spring Data for Apache Cassandra 2.1 * New annotations for `@CountQuery` and `@ExistsQuery`. diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 0111a3fc0..2de17c6e7 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -414,6 +414,7 @@ to indicate either a single or a composite (compound) primary key. If used on a * `@PrimaryKeyClass`: Applied at the class level to indicate that this class is a compound primary key class. Must be referenced with `@PrimaryKey` in the entity class. * `@Transient`: By default, all private fields are mapped to the row. This annotation excludes the field +* `@ReadOnlyProperty`: Applies at the field level to mark a property as read-only. Entity-bound insert and update statements do not include this property. where it is applied from being stored in the database. * `@Column`: Applied at the field level. Describes the column name as it is represented in the Cassandra table, thus letting the name differ from the field name of the class.