From a430e808f0a2d0b008295ed66dbc9e77a3254644 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Apr 2017 13:58:55 +0200 Subject: [PATCH] DATACASS-420 - Do not insert null values. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../convert/MappingCassandraConverter.java | 4 + .../query/CassandraEntityInformation.java | 12 +- .../MappingCassandraEntityInformation.java | 31 +++- .../support/SimpleCassandraRepository.java | 13 +- .../SimpleReactiveCassandraRepository.java | 10 +- ...CassandraConverterUDTIntegrationTests.java | 16 +- .../CassandraTemplateIntegrationTests.java | 11 +- ...ngCassandraEntityInformationUnitTests.java | 106 +++++++++++++ .../SimpleCassandraRepositoryUnitTests.java | 139 ++++++++++++++++++ 9 files changed, 319 insertions(+), 23 deletions(-) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/MappingCassandraEntityInformationUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/SimpleCassandraRepositoryUnitTests.java 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; + } +}