DATAREDIS-543 - Allow customized type aliasing.

We now support customization of type aliasing for types using through Redis Repositories. Type aliasing can be customized by either annotating types with @TypeAlias or by providing a RedisTypeMapper to MappingRedisConverter.

Map<Class<?>, String> mapping = new HashMap<>();

mapping.put(Person.class, "person");

ConfigurableTypeInformationMapper mapper = new ConfigurableTypeInformationMapper(mapping);
RedisTypeMapper typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, Collections.singletonList(mapper))

alternatively:

@RedisHash
@TypeAlias("person")
class Person {
  // …
}

Original Pull Request: #299
This commit is contained in:
Mark Paluch
2017-12-20 14:28:01 +01:00
committed by Christoph Strobl
parent f3128bf0c0
commit 04153e07b0
9 changed files with 696 additions and 100 deletions

View File

@@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.index.Indexed;
@@ -109,6 +110,7 @@ public class ConversionTestEntities {
}
}
@TypeAlias("with-post-code")
public static class AddressWithPostcode extends Address {
String postcode;

View File

@@ -0,0 +1,224 @@
/*
* 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.redis.core.convert;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.ConfigurableTypeInformationMapper;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* Unit tests for {@link DefaultRedisTypeMapper}.
*
* @author Mark Paluch
*/
public class DefaultRedisTypeMapperUnitTests {
GenericConversionService conversionService;
ConfigurableTypeInformationMapper configurableTypeInformationMapper;
SimpleTypeInformationMapper simpleTypeInformationMapper;
DefaultRedisTypeMapper typeMapper;
@Before
public void setUp() {
conversionService = new GenericConversionService();
new RedisCustomConversions().registerConvertersIn(conversionService);
configurableTypeInformationMapper = new ConfigurableTypeInformationMapper(singletonMap(String.class, "1"));
simpleTypeInformationMapper = new SimpleTypeInformationMapper();
typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY);
}
@Test // DATAREDIS-543
public void defaultInstanceWritesClasses() {
writesTypeToField(new Bucket(), String.class, String.class.getName());
}
@Test // DATAREDIS-543
public void defaultInstanceReadsClasses() {
Bucket bucket = Bucket
.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, String.class.getName()));
readsTypeFromField(bucket, String.class);
}
@Test // DATAREDIS-543
public void writesMapKeyForType() {
typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY,
Collections.singletonList(configurableTypeInformationMapper));
writesTypeToField(new Bucket(), String.class, "1");
writesTypeToField(new Bucket(), Object.class, null);
}
@Test // DATAREDIS-543
public void writesClassNamesForUnmappedValuesIfConfigured() {
typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY,
Arrays.asList(configurableTypeInformationMapper, simpleTypeInformationMapper));
writesTypeToField(new Bucket(), String.class, "1");
writesTypeToField(new Bucket(), Object.class, Object.class.getName());
}
@Test // DATAREDIS-543
public void readsTypeForMapKey() {
typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY,
Collections.singletonList(configurableTypeInformationMapper));
readsTypeFromField(Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, "1")),
String.class);
readsTypeFromField(Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, "unmapped")),
null);
}
@Test // DATAREDIS-543
public void readsTypeLoadingClassesForUnmappedTypesIfConfigured() {
typeMapper = new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY,
Arrays.asList(configurableTypeInformationMapper, simpleTypeInformationMapper));
readsTypeFromField(new Bucket(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, "1".getBytes())), String.class);
readsTypeFromField(
new Bucket(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, Object.class.getName().getBytes())),
Object.class);
}
@Test // DATAREDIS-543
public void addsFullyQualifiedClassNameUnderDefaultKeyByDefault() {
writesTypeToField(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, new Bucket(), String.class);
}
@Test // DATAREDIS-543
public void writesTypeToCustomFieldIfConfigured() {
typeMapper = new DefaultRedisTypeMapper("_custom");
writesTypeToField("_custom", new Bucket(), String.class);
}
@Test // DATAREDIS-543
public void doesNotWriteTypeInformationInCaseKeyIsSetToNull() {
typeMapper = new DefaultRedisTypeMapper(null);
writesTypeToField(null, new Bucket(), String.class);
}
@Test // DATAREDIS-543
public void readsTypeFromDefaultKeyByDefault() {
readsTypeFromField(
Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, String.class.getName())),
String.class);
}
@Test // DATAREDIS-543
public void readsTypeFromCustomFieldConfigured() {
typeMapper = new DefaultRedisTypeMapper("_custom");
readsTypeFromField(Bucket.newBucketFromStringMap(singletonMap("_custom", String.class.getName())), String.class);
}
@Test // DATAREDIS-543
public void returnsListForBasicDBLists() {
readsTypeFromField(new Bucket(), null);
}
@Test // DATAREDIS-543
public void returnsNullIfNoTypeInfoInBucket() {
readsTypeFromField(new Bucket(), null);
readsTypeFromField(Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, "")), null);
}
@Test // DATAREDIS-543
public void returnsNullIfClassCannotBeLoaded() {
readsTypeFromField(Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, "fooBar")),
null);
}
@Test // DATAREDIS-543
public void returnsNullIfTypeKeySetToNull() {
typeMapper = new DefaultRedisTypeMapper(null);
readsTypeFromField(
Bucket.newBucketFromStringMap(singletonMap(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, String.class.getName())),
null);
}
@Test // DATAREDIS-543
public void returnsCorrectTypeKey() {
assertThat(typeMapper.isTypeKey(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY)).isTrue();
typeMapper = new DefaultRedisTypeMapper("_custom");
assertThat(typeMapper.isTypeKey("_custom")).isTrue();
assertThat(typeMapper.isTypeKey(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY)).isFalse();
typeMapper = new DefaultRedisTypeMapper(null);
assertThat(typeMapper.isTypeKey("_custom")).isFalse();
assertThat(typeMapper.isTypeKey(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY)).isFalse();
}
private void readsTypeFromField(Bucket bucket, @Nullable Class<?> type) {
TypeInformation<?> typeInfo = typeMapper.readType(BucketPropertyPath.from(bucket));
if (type != null) {
assertThat(typeInfo).isNotNull();
assertThat(typeInfo.getType()).isAssignableFrom(type);
} else {
assertThat(typeInfo).isNull();
}
}
private void writesTypeToField(@Nullable String field, Bucket bucket, Class<?> type) {
typeMapper.writeType(type, BucketPropertyPath.from(bucket));
if (field == null) {
assertThat(bucket.keySet()).isEmpty();
} else {
assertThat(bucket.asMap()).containsKey(field);
assertThat(bucket.get(field)).isEqualTo(type.getName().getBytes());
}
}
private void writesTypeToField(Bucket bucket, Class<?> type, @Nullable Object value) {
typeMapper.writeType(type, BucketPropertyPath.from(bucket));
if (value == null) {
assertThat(bucket.keySet()).isEmpty();
} else {
byte[] expected = value instanceof Class ? ((Class) value).getName().getBytes() : value.toString().getBytes();
assertThat(bucket.asMap()).containsKey(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY);
assertThat(bucket.get(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY)).isEqualTo(expected);
}
}
}

View File

@@ -32,16 +32,7 @@ import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import org.hamcrest.core.IsEqual;
import org.junit.Before;
@@ -94,6 +85,15 @@ public class MappingRedisConverterUnitTests {
assertThat(write(rand).getBucket(), isBucket().containingTypeHint("_class", Person.class));
}
@Test // DATAREDIS-543
public void writeSkipsTypeHintIfConfigured() {
converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock);
converter.afterPropertiesSet();
assertThat(write(rand).getBucket(), isBucket().containingTypeHint("_class", Person.class));
}
@Test // DATAREDIS-425
public void writeAppendsKeyCorrectly() {
@@ -205,7 +205,7 @@ public class MappingRedisConverterUnitTests {
assertThat(target.getBucket(), isBucket().without("address._class"));
}
@Test // DATAREDIS-425
@Test // DATAREDIS-425, DATAREDIS-543
public void writeAddsClassTypeInformationCorrectlyForNonMatchingTypes() {
AddressWithPostcode address = new AddressWithPostcode();
@@ -216,7 +216,7 @@ public class MappingRedisConverterUnitTests {
RedisData target = write(rand);
assertThat(target.getBucket(), isBucket().containingTypeHint("address._class", AddressWithPostcode.class));
assertThat(target.getBucket(), isBucket().containingUtf8String("address._class", "with-post-code"));
}
@Test // DATAREDIS-425

View File

@@ -15,19 +15,38 @@
*/
package org.springframework.data.redis.repository;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.convert.ConfigurableTypeInformationMapper;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.DefaultRedisTypeMapper;
import org.springframework.data.redis.core.convert.MappingRedisConverter;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.core.convert.RedisTypeMapper;
import org.springframework.data.redis.core.convert.ReferenceResolver;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@@ -45,10 +64,52 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
public MappingRedisConverter redisConverter(RedisMappingContext mappingContext,
RedisCustomConversions customConversions, ReferenceResolver referenceResolver) {
MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver,
customTypeMapper());
mappingRedisConverter.setCustomConversions(customConversions);
return mappingRedisConverter;
}
private RedisTypeMapper customTypeMapper() {
Map<Class<?>, String> mapping = new HashMap<>();
mapping.put(Person.class, "person");
mapping.put(City.class, "city");
ConfigurableTypeInformationMapper mapper = new ConfigurableTypeInformationMapper(mapping);
return new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, Collections.singletonList(mapper));
}
}
@Autowired RedisOperations<String, String> operations;
@Test // DATAREDIS-543
public void shouldConsiderCustomTypeMapper() {
Person rand = new Person();
rand.id = "rand";
rand.firstname = "rand";
rand.lastname = "al'thor";
repo.save(rand);
Map<String, String> entries = operations.<String, String> opsForHash().entries("persons:rand");
assertThat(entries.get("_class"), is(equalTo("person")));
}
}