diff --git a/pom.xml b/pom.xml index e6d868411f..33d4a70be2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 1.8.0.BUILD-SNAPSHOT + 1.8.0.DATAREDIS-503-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc index 5fc66ab50c..52453487e3 100644 --- a/src/main/asciidoc/reference/redis.adoc +++ b/src/main/asciidoc/reference/redis.adoc @@ -50,13 +50,13 @@ http://github.com/xetorthio/jedis[Jedis] is one of the connectors supported by t [source,xml] ---- - - + - + ---- @@ -64,14 +64,14 @@ For production use however, one might want to tweak the settings such as the hos [source,xml] ---- - - + - + - + ---- @@ -85,13 +85,13 @@ A typical JRedis configuration can looks like this: [source,xml] ---- - - + - + ---- @@ -101,9 +101,9 @@ The configuration is quite similar to Jedis, with one notable exception. By defa ---- - + @@ -112,7 +112,7 @@ The configuration is quite similar to Jedis, with one notable exception. By defa - + ---- @@ -125,14 +125,14 @@ By now, its configuration is probably easy to guess: [source,xml] ---- - - + - + - + ---- @@ -148,13 +148,13 @@ Its configuration is probably easy to guess: [source,xml] ---- - - + - + ---- @@ -172,12 +172,12 @@ NOTE: Please note that currently only http://github.com/xetorthio/jedis[Jedis] a /** * jedis */ -@Bean +@Bean public RedisConnectionFactory jedisConnectionFactory() { RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() .master("mymaster") .sentinel("127.0.0.1", 26379) .sentinel("127.0.0.1", 26380); return new JedisConnectionFactory(sentinelConfig); -} +} /** * lettuce @@ -262,33 +262,33 @@ For cases where a certain template *view* is needed, declare the view as a depen [source,xml] ---- - - + - ... - + ... + ---- [source,java] ---- -public class Example { - - // inject the actual template - @Autowired - private RedisTemplate template; +public class Example { + + // inject the actual template + @Autowired + private RedisTemplate template; // inject the template as ListOperations - @Resource(name="redisTemplate") + @Resource(name="redisTemplate") private ListOperations listOps; public void addLink(String userId, URL url) { - listOps.leftPush(userId, url.toExternalForm()); + listOps.leftPush(userId, url.toExternalForm()); } } ---- @@ -301,27 +301,27 @@ Since it's quite common for the keys and values stored in Redis to be `java.lang [source,xml] ---- - - + - + - ... + ... ---- [source,java] ---- -public class Example { - +public class Example { + @Autowired - private StringRedisTemplate redisTemplate; - + private StringRedisTemplate redisTemplate; + public void addLink(String userId, URL url) { - redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); + redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); } } ---- @@ -330,12 +330,12 @@ As with the other Spring templates, `RedisTemplate` and `StringRedisTemplate` al [source,java] ---- -public void useCallback() { +public void useCallback() { - redisTemplate.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { + redisTemplate.execute(new RedisCallback() { + public Object doInRedis(RedisConnection connection) throws DataAccessException { Long size = connection.dbSize(); - // Can cast to StringRedisConnection if using a StringRedisTemplate + // Can cast to StringRedisConnection if using a StringRedisTemplate ((StringRedisConnection)connection).set("key", "value"); } }); @@ -345,7 +345,57 @@ public void useCallback() { [[redis:serializer]] == Serializers -From the framework perspective, the data stored in Redis is just bytes. While Redis itself supports various types, for the most part these refer to the way the data is stored rather than what it represents. It is up to the user to decide whether the information gets translated into Strings or any other objects. The conversion between the user (custom) types and raw data (and vice-versa) is handled in Spring Data Redis through the `RedisSerializer` interface (package `org.springframework.data.redis.serializer`) which as the name implies, takes care of the serialization process. Multiple implementations are available out of the box, two of which have been already mentioned before in this documentation: the `StringRedisSerializer` and the `JdkSerializationRedisSerializer`. However one can use `OxmSerializer` for Object/XML mapping through Spring 3 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[OXM] support or either `JacksonJsonRedisSerializer`, `Jackson2JsonRedisSerializer` or `GenericJackson2JsonRedisSerializer for storing data in http://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values - it can be used for keys, values or hashes without any restrictions. +From the framework perspective, the data stored in Redis is just bytes. While Redis itself supports various types, for the most part these refer to the way the data is stored rather than what it represents. It is up to the user to decide whether the information gets translated into Strings or any other objects. The conversion between the user (custom) types and raw data (and vice-versa) is handled in Spring Data Redis through the `RedisSerializer` interface (package `org.springframework.data.redis.serializer`) which as the name implies, takes care of the serialization process. Multiple implementations are available out of the box, two of which have been already mentioned before in this documentation: the `StringRedisSerializer` and the `JdkSerializationRedisSerializer`. However one can use `OxmSerializer` for Object/XML mapping through Spring 3 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[OXM] support or either `JacksonJsonRedisSerializer`, `Jackson2JsonRedisSerializer` or `GenericJackson2JsonRedisSerializer` for storing data in http://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values - it can be used for keys, values or hashes without any restrictions.[[redis:serializer]] + +== Hash mapping + +Data can be stored using various data structures within Redis. You already learned about `Jackson2JsonRedisSerializer` which can convert objects +in http://en.wikipedia.org/wiki/JSON[JSON] format. JSON can be ideally stored as value using plain keys. A more sophisticated mapping of structured objects +can be achieved using Redis Hashes. Spring Data Redis offers various strategies for mapping data to hashes depending on the use case. + +1. Direct mapping using `HashOperations` and a <> +2. Using <> +3. Using `HashMapper` and `HashOperations` + +=== Hash mappers + +Hash mappers are converters to map objects to a `Map` and back. `HashMapper` is intended for using with Redis Hashes. + +Multiple implementations are available out of the box: + +1. `BeanUtilsHashMapper` using Spring's http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html[BeanUtils] +2. `ObjectHashMapper` using <> + +[source,java] +---- +public class Person { + String firstname; + String lastname; + + // … +} + +public class HashMapping { + + @Autowired + HashOperations hashOperations; + + HashMapper mapper = new ObjectHashMapper(); + + public void writeHash(String key, Person person) { + + Map mappedHash = mapper.toHash(person); + hashOperations.putAll(key, mappedHash); + } + + public Person loadHash(String key) { + + Map loadedHash = hashOperations.entries("key"); + return (Person) mapper.fromHash(loadedHash); + } +} +---- + :leveloffset: 2 include::{referenceDir}/redis-messaging.adoc[] @@ -371,27 +421,27 @@ _FIFO (First-In-First-Out)_, _LIFO (Last-In-First-Out)_ or _capped collection_ w [source,xml] ---- - - + + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - - - + + + - + ---- [source,java] ---- -public class AnotherExample { - +public class AnotherExample { + // injected private Deque queue; - + public void addTag(String tag) { queue.push(tag); } @@ -408,18 +458,18 @@ Spring Redis provides an implementation for Spring http://docs.spring.io/spring/ [source,xml] ---- + http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> - - + + - - - + + + ---- NOTE: By default `RedisCacheManager` will lazily initialize `RedisCache` whenever a `Cache` is requested. This can be changed by predefining a `Set` of cache names. diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index 5491356215..bff2eb4e8f 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -344,6 +344,14 @@ public void write(Object source, final RedisData sink) { if (!customConversions.hasCustomWriteTarget(source.getClass())) { typeMapper.writeType(ClassUtils.getUserClass(source), sink); } + + if (entity == null) { + + typeMapper.writeType(ClassUtils.getUserClass(source), sink); + sink.getBucket().put("_raw", conversionService.convert(source, byte[].class)); + return; + } + sink.setKeyspace(entity.getKeySpace()); writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink); diff --git a/src/main/java/org/springframework/data/redis/hash/HashMapper.java b/src/main/java/org/springframework/data/redis/hash/HashMapper.java index 711b15b77c..2d696ece2d 100644 --- a/src/main/java/org/springframework/data/redis/hash/HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/HashMapper.java @@ -1,12 +1,12 @@ /* - * Copyright 2011-2013 the original author or authors. - * + * Copyright 2011-2016 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. @@ -20,12 +20,28 @@ /** * Core mapping contract between Java types and Redis hashes/maps. It's up to the implementation to support nested * objects. - * + * + * @param Object type + * @param Redis Hash field type + * @param Redis Hash value type * @author Costin Leau + * @author Mark Paluch */ public interface HashMapper { + /** + * Convert an {@code object} to a map that can be used with Redis hashes. + * + * @param object + * @return + */ Map toHash(T object); + /** + * Convert a {@code hash} (map) to an object. + * + * @param hash + * @return + */ T fromHash(Map hash); } diff --git a/src/main/java/org/springframework/data/redis/hash/ObjectHashMapper.java b/src/main/java/org/springframework/data/redis/hash/ObjectHashMapper.java new file mode 100644 index 0000000000..939938f9f3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/hash/ObjectHashMapper.java @@ -0,0 +1,153 @@ +/* + * Copyright 2016 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.hash; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.redis.core.convert.CustomConversions; +import org.springframework.data.redis.core.convert.IndexResolver; +import org.springframework.data.redis.core.convert.IndexedData; +import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.RedisData; +import org.springframework.data.redis.core.convert.ReferenceResolver; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.TypeInformation; + +/** + * {@link HashMapper} based on {@link MappingRedisConverter}. Supports nested properties and simple types like + * {@link String}. + * + *
+ * 
+ * class Person {
+ *
+ *   String firstname;
+ *   String lastname;
+ *
+ *   List<String> nicknames;
+ *   List<Person> coworkers;
+ *
+ *   Address address;
+ * }
+ * 
+ * 
+ * + * The above is represented as: + * + *
+ * 
+ * _class=org.example.Person
+ * firstname=rand
+ * lastname=al'thor
+ * coworkers.[0].firstname=mat
+ * coworkers.[0].nicknames.[0]=prince of the ravens
+ * coworkers.[1].firstname=perrin
+ * coworkers.[1].address.city=two rivers
+ * 
+ * 
+ * + * @author Christoph Strobl + * @since 1.8 + */ +public class ObjectHashMapper implements HashMapper { + + private final MappingRedisConverter converter; + + /** + * Creates new {@link ObjectHashMapper}. + */ + public ObjectHashMapper() { + this(new CustomConversions()); + } + + /** + * Creates new {@link ObjectHashMapper}. + * + * @param customConversions can be {@literal null}. + */ + public ObjectHashMapper(CustomConversions customConversions) { + + MappingRedisConverter mappingConverter = new MappingRedisConverter(new RedisMappingContext(), + new NoOpIndexResolver(), new NoOpReferenceResolver()); + mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); + mappingConverter.afterPropertiesSet(); + + converter = mappingConverter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.hash.HashMapper#toHash(java.lang.Object) + */ + @Override + public Map toHash(Object source) { + + if (source == null) { + return Collections.emptyMap(); + } + + RedisData sink = new RedisData(); + converter.write(source, sink); + return sink.getBucket().rawMap(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.hash.HashMapper#fromHash(java.util.Map) + */ + @Override + public Object fromHash(Map hash) { + + if (hash == null || hash.isEmpty()) { + return null; + } + + return converter.read(Object.class, new RedisData(hash)); + } + + /** + * {@link ReferenceResolver} implementation always returning an empty {@link Map}. + * + * @author Christoph Strobl + */ + private static class NoOpReferenceResolver implements ReferenceResolver { + + private static final Map NO_REFERENCE = Collections.emptyMap(); + + @Override + public Map resolveReference(Serializable id, String keyspace) { + return NO_REFERENCE; + } + } + + /** + * {@link IndexResolver} always returning an empty {@link Set}. + * + * @author Christoph Strobl + */ + private static class NoOpIndexResolver implements IndexResolver { + + private static final Set NO_INDEXES = Collections.emptySet(); + + @Override + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + return NO_INDEXES; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java b/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java new file mode 100644 index 0000000000..223a9f6955 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.mapping; + +import org.junit.Test; +import org.springframework.data.redis.hash.ObjectHashMapper; + +/** + * @author Christoph Strobl + */ +public class ObjectHashMapperTests extends AbstractHashMapperTest { + + protected ObjectHashMapper mapperFor(Class t) { + return new ObjectHashMapper(); + } + + /** + * @see DATAREDIS-503 + */ + @Test + public void testSimpleType() { + assertBackAndForwardMapping(new Integer(100)); + } +}