DATAREDIS-849 - Add support for immutable objects.
We now support immutable objects for saving (e.g. object without a provided Id) and loading (i.e. persistence constructor declares a subset of properties). New instances are created using wither methods/Kotlin copy(…) methods if an immutable object requires association with an Id. Association of the Id to its object moved from RedisKeyValueAdapter to RedisKeyValueTemplate.
This commit is contained in:
committed by
Oliver Gierke
parent
3defab118e
commit
322cfabb17
@@ -36,7 +36,7 @@ import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.KeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
@@ -217,15 +217,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
}
|
||||
|
||||
if (rdo.getId() == null) {
|
||||
|
||||
rdo.setId(converter.getConversionService().convert(id, String.class));
|
||||
|
||||
if (!(item instanceof RedisData)) {
|
||||
KeyValuePersistentProperty idProperty = converter.getMappingContext()
|
||||
.getRequiredPersistentEntity(item.getClass()).getRequiredIdProperty();
|
||||
converter.getMappingContext().getRequiredPersistentEntity(item.getClass()).getPropertyAccessor(item)
|
||||
.setProperty(idProperty, id);
|
||||
}
|
||||
}
|
||||
|
||||
redisOps.execute((RedisCallback<Object>) connection -> {
|
||||
@@ -624,8 +616,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
});
|
||||
|
||||
if (timeout != null || !ttlProperty.getType().isPrimitive()) {
|
||||
entity.getPropertyAccessor(target).setProperty(ttlProperty,
|
||||
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(target);
|
||||
|
||||
propertyAccessor.setProperty(ttlProperty,
|
||||
converter.getConversionService().convert(timeout, ttlProperty.getType()));
|
||||
|
||||
target = propertyAccessor.getBean();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,12 @@ import java.util.List;
|
||||
import org.springframework.data.keyvalue.core.KeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.KeyValueCallback;
|
||||
import org.springframework.data.keyvalue.core.KeyValueTemplate;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.redis.core.convert.RedisConverter;
|
||||
import org.springframework.data.redis.core.convert.RedisData;
|
||||
import org.springframework.data.redis.core.mapping.RedisMappingContext;
|
||||
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -127,14 +131,31 @@ public class RedisKeyValueTemplate extends KeyValueTemplate {
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueTemplate#insert(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void insert(Object id, Object objectToInsert) {
|
||||
public <T> T insert(Object id, T objectToInsert) {
|
||||
|
||||
if (objectToInsert instanceof PartialUpdate) {
|
||||
doPartialUpdate((PartialUpdate<?>) objectToInsert);
|
||||
return;
|
||||
return objectToInsert;
|
||||
}
|
||||
|
||||
super.insert(id, objectToInsert);
|
||||
if (!(objectToInsert instanceof RedisData)) {
|
||||
|
||||
RedisConverter converter = adapter.getConverter();
|
||||
|
||||
RedisPersistentEntity<?> entity = converter.getMappingContext()
|
||||
.getRequiredPersistentEntity(objectToInsert.getClass());
|
||||
|
||||
KeyValuePersistentProperty idProperty = entity.getRequiredIdProperty();
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(objectToInsert);
|
||||
|
||||
if (propertyAccessor.getProperty(idProperty) == null) {
|
||||
|
||||
propertyAccessor.setProperty(idProperty, id);
|
||||
return super.insert(id, propertyAccessor.getBean());
|
||||
}
|
||||
}
|
||||
|
||||
return super.insert(id, objectToInsert);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -142,14 +163,20 @@ public class RedisKeyValueTemplate extends KeyValueTemplate {
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueTemplate#update(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void update(Object objectToUpdate) {
|
||||
public <T> T update(T objectToUpdate) {
|
||||
|
||||
if (objectToUpdate instanceof PartialUpdate) {
|
||||
doPartialUpdate((PartialUpdate<?>) objectToUpdate);
|
||||
return;
|
||||
|
||||
return objectToUpdate;
|
||||
}
|
||||
|
||||
super.update(objectToUpdate);
|
||||
return super.update(objectToUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T update(Object id, T objectToUpdate) {
|
||||
return super.update(id, objectToUpdate);
|
||||
}
|
||||
|
||||
protected void doPartialUpdate(final PartialUpdate<?> update) {
|
||||
|
||||
@@ -211,7 +211,11 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
R instance = (R) conversionService.convert(partial, readType.getType());
|
||||
|
||||
if (entity != null && entity.hasIdProperty()) {
|
||||
entity.getPropertyAccessor(instance).setProperty(entity.getRequiredIdProperty(), source.getId());
|
||||
|
||||
PersistentPropertyAccessor<R> propertyAccessor = entity.getPropertyAccessor(instance);
|
||||
|
||||
propertyAccessor.setProperty(entity.getRequiredIdProperty(), source.getId());
|
||||
instance = propertyAccessor.getBean();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
@@ -246,7 +250,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
|
||||
readAssociation(path, source, entity, accessor);
|
||||
|
||||
return (R) instance;
|
||||
return (R) accessor.getBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -19,8 +19,10 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.number.IsCloseTo.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -805,6 +807,33 @@ public class RedisKeyValueTemplateTests {
|
||||
assertThat(target.get().ttl, is(-1L));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-849
|
||||
public void shouldWriteImmutableType() {
|
||||
|
||||
ImmutableObject source = new ImmutableObject().withValue("foo").withTtl(1234L);
|
||||
|
||||
ImmutableObject inserted = template.insert(source);
|
||||
|
||||
assertThat(source.id, is(nullValue()));
|
||||
assertThat(inserted.id, is(notNullValue()));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-849
|
||||
public void shouldReadImmutableType() {
|
||||
|
||||
ImmutableObject source = new ImmutableObject().withValue("foo").withTtl(1234L);
|
||||
ImmutableObject inserted = template.insert(source);
|
||||
|
||||
Optional<ImmutableObject> loaded = template.findById(inserted.id, ImmutableObject.class);
|
||||
|
||||
assertThat(loaded.isPresent(), is(true));
|
||||
|
||||
ImmutableObject immutableObject = loaded.get();
|
||||
assertThat(immutableObject.id, is(inserted.id));
|
||||
assertThat(immutableObject.ttl, is(notNullValue()));
|
||||
assertThat(immutableObject.value, is(inserted.value));
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@RedisHash("template-test-type-mapping")
|
||||
static class VariousTypes {
|
||||
@@ -929,4 +958,20 @@ public class RedisKeyValueTemplateTests {
|
||||
String value;
|
||||
@TimeToLive int ttl;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Wither
|
||||
@AllArgsConstructor
|
||||
static class ImmutableObject {
|
||||
|
||||
final @Id String id;
|
||||
final String value;
|
||||
final @TimeToLive Long ttl;
|
||||
|
||||
ImmutableObject() {
|
||||
this.id = null;
|
||||
this.value = null;
|
||||
this.ttl = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryInteg
|
||||
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class,
|
||||
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
|
||||
classes = { PersonRepository.class, CityRepository.class }) })
|
||||
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -19,6 +19,8 @@ import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -60,6 +62,7 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
|
||||
@Autowired PersonRepository repo;
|
||||
@Autowired CityRepository cityRepo;
|
||||
@Autowired ImmutableObjectRepository immutableObjectRepo;
|
||||
@Autowired KeyValueTemplate kvTemplate;
|
||||
|
||||
@Before
|
||||
@@ -403,6 +406,37 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
assertThat(result, not(hasItems(p1)));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-849
|
||||
public void shouldReturnNewObjectInstanceOnImmutableSave() {
|
||||
|
||||
Immutable object = new Immutable(null, "Walter", new Immutable("heisenberg", "White", null));
|
||||
Immutable saved = immutableObjectRepo.save(object);
|
||||
|
||||
assertThat(object.id, is(nullValue()));
|
||||
assertThat(saved.id, is(notNullValue()));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-849
|
||||
public void shouldReturnNewObjectInstanceOnImmutableSaveAll() {
|
||||
|
||||
Immutable object = new Immutable(null, "Walter", new Immutable("heisenberg", "White", null));
|
||||
List<Immutable> saved = (List) immutableObjectRepo.saveAll(Collections.singleton(object));
|
||||
|
||||
assertThat(object.id, is(nullValue()));
|
||||
assertThat(saved.get(0).id, is(notNullValue()));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-849
|
||||
public void shouldProperlyReadNestedImmutableObject() {
|
||||
|
||||
Immutable nested = new Immutable("heisenberg", "White", null);
|
||||
Immutable object = new Immutable(null, "Walter", nested);
|
||||
Immutable saved = immutableObjectRepo.save(object);
|
||||
|
||||
Immutable loaded = immutableObjectRepo.findById(saved.id).get();
|
||||
assertThat(loaded.nested, is(nested));
|
||||
}
|
||||
|
||||
public static interface PersonRepository
|
||||
extends PagingAndSortingRepository<Person, String>, QueryByExampleExecutor<Person> {
|
||||
|
||||
@@ -434,11 +468,13 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
<S extends Person> List<S> findAll(Example<S> example);
|
||||
}
|
||||
|
||||
public static interface CityRepository extends CrudRepository<City, String> {
|
||||
public interface CityRepository extends CrudRepository<City, String> {
|
||||
|
||||
List<City> findByLocationNear(Point point, Distance distance);
|
||||
}
|
||||
|
||||
public interface ImmutableObjectRepository extends CrudRepository<Immutable, String> {}
|
||||
|
||||
/**
|
||||
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
|
||||
*
|
||||
@@ -493,4 +529,14 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
|
||||
@GeoIndexed Point location;
|
||||
}
|
||||
|
||||
@Value
|
||||
@Wither
|
||||
public static class Immutable {
|
||||
|
||||
@Id String id;
|
||||
String name;
|
||||
|
||||
Immutable nested;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
|
||||
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class,
|
||||
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
|
||||
classes = { PersonRepository.class, CityRepository.class }) })
|
||||
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
|
||||
|
||||
static class Config {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user