Skip to content

Commit 8f129b1

Browse files
DATAREDIS-425 - Preserve item order in list, update documentation.
We now preserve the item order when converting list elements. This does not mean that we place elements at the exact position retrieved from the store but rather maintain their order based on the index value. Additionally applied some documentation polishing and cluster tests. Original Pull Request: #156
1 parent 9eb309c commit 8f129b1

File tree

6 files changed

+627
-357
lines changed

6 files changed

+627
-357
lines changed

src/main/asciidoc/reference/redis-repositories.adoc

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,8 @@ public class ApplicationConfig {
383383
public static class MyIndexConfiguration extends IndexConfiguration {
384384
385385
@Override
386-
protected Iterable<RedisIndexSetting> initialConfiguration() {
387-
return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
386+
protected Iterable<IndexDefinition> initialConfiguration() {
387+
return Collections.singleton(new SimpleIndexDefinition("persons", "firstname"));
388388
}
389389
}
390390
}
@@ -411,8 +411,8 @@ public class ApplicationConfig {
411411
public static class MyIndexConfiguration extends IndexConfiguration {
412412
413413
@Override
414-
protected Iterable<RedisIndexSetting> initialConfiguration() {
415-
return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
414+
protected Iterable<IndexDefinition> initialConfiguration() {
415+
return Collections.singleton(new SimpleIndexDefinition("persons", "firstname"));
416416
}
417417
}
418418
}
@@ -537,7 +537,38 @@ Here's an overview of the keywords supported for Redis and what a method contain
537537
|===============
538538
====
539539

540-
[[redis.misc.cdi-integration]]
540+
[[redis.repositories.cluster]]
541+
== Redis Repositories running on Cluster
542+
543+
Using the Redis repository support in a clustered Redis environment is fine. Please see the <<cluster, Redis Cluster>> section for `ConnectionFactory` configuration details.
544+
Still some considerations have to be done as the default key distribution will spread entities and secondary indexes through out the whole cluster and its slots.
545+
546+
[options = "header, autowidth"]
547+
|===============
548+
|key|type|slot|node
549+
|persons:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|15171|127.0.0.1:7381
550+
|persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|7373|127.0.0.1:7380
551+
|persons:firstname:rand|index|1700|127.0.0.1:7379
552+
|
553+
|===============
554+
====
555+
556+
Some commands like `SINTER` and `SUNION` can only be processed on the Server side when all involved keys map to the same slot. Otherwise computation has to be done on client side.
557+
Therefore it be useful to pin keyspaces to a single slot which allows to make use of Redis serverside computation right away.
558+
559+
[options = "header, autowidth"]
560+
|===============
561+
|key|type|slot|node
562+
|{persons}:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|2399|127.0.0.1:7379
563+
|{persons}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|2399|127.0.0.1:7379
564+
|{persons}:firstname:rand|index|2399|127.0.0.1:7379
565+
|
566+
|===============
567+
====
568+
569+
TIP: Define and pin keyspaces via `@RedisHash("{yourkeyspace}") to specific slots when using Redis cluster.
570+
571+
[[redis.repositories.cdi-integration]]
541572
== CDI integration
542573

543574
Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. There's sophisticated support to easily set up Spring to create bean instances. Spring Data Redis ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data Redis JAR into your classpath.

src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package org.springframework.data.redis.core.convert;
1717

18+
import java.util.ArrayList;
1819
import java.util.Collection;
20+
import java.util.Comparator;
1921
import java.util.HashMap;
22+
import java.util.List;
2023
import java.util.Map;
2124
import java.util.Map.Entry;
2225
import java.util.Set;
@@ -53,6 +56,7 @@
5356
import org.springframework.util.ClassUtils;
5457
import org.springframework.util.CollectionUtils;
5558
import org.springframework.util.StringUtils;
59+
import org.springframework.util.comparator.NullSafeComparator;
5660

5761
/**
5862
* {@link RedisConverter} implementation creating flat binary map structure out of a given domain type. Considers
@@ -103,6 +107,9 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
103107
private final GenericConversionService conversionService;
104108
private final EntityInstantiators entityInstantiators;
105109
private final TypeMapper<RedisData> typeMapper;
110+
private final Comparator<String> listKeyComparator = new NullSafeComparator<String>(
111+
NaturalOrderingKeyComparator.INSTANCE, true);
112+
106113
private ReferenceResolver referenceResolver;
107114
private IndexResolver indexResolver;
108115
private CustomConversions customConversions;
@@ -533,11 +540,15 @@ private Collection<?> readCollectionOfSimpleTypes(String path, Class<?> collecti
533540

534541
Bucket partial = source.getBucket().extract(path + ".[");
535542

543+
List<String> keys = new ArrayList<String>(partial.keySet());
544+
keys.sort(listKeyComparator);
545+
536546
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, partial.size());
537547

538-
for (byte[] value : partial.values()) {
539-
target.add(fromBytes(value, valueType));
548+
for (String key : keys) {
549+
target.add(fromBytes(partial.get(key), valueType));
540550
}
551+
541552
return target;
542553
}
543554

@@ -551,7 +562,8 @@ private Collection<?> readCollectionOfSimpleTypes(String path, Class<?> collecti
551562
private Collection<?> readCollectionOfComplexTypes(String path, Class<?> collectionType, Class<?> valueType,
552563
Bucket source) {
553564

554-
Set<String> keys = source.extractAllKeysFor(path);
565+
List<String> keys = new ArrayList<String>(source.extractAllKeysFor(path));
566+
keys.sort(listKeyComparator);
555567

556568
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, keys.size());
557569

@@ -810,4 +822,90 @@ public String resolveKeySpace(Class<?> type) {
810822
}
811823
}
812824

825+
private enum NaturalOrderingKeyComparator implements Comparator<String> {
826+
827+
INSTANCE;
828+
829+
/*
830+
* (non-Javadoc)
831+
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
832+
*/
833+
public int compare(String s1, String s2) {
834+
835+
int s1offset = 0;
836+
int s2offset = 0;
837+
838+
while (s1offset < s1.length() && s2offset < s2.length()) {
839+
840+
Part thisPart = extractPart(s1, s1offset);
841+
Part thatPart = extractPart(s2, s2offset);
842+
843+
int result = thisPart.compareTo(thatPart);
844+
845+
if (result != 0) {
846+
return result;
847+
}
848+
849+
s1offset += thisPart.length();
850+
s2offset += thatPart.length();
851+
}
852+
853+
return 0;
854+
}
855+
856+
private Part extractPart(String source, int offset) {
857+
858+
StringBuilder builder = new StringBuilder();
859+
860+
char c = source.charAt(offset);
861+
builder.append(c);
862+
863+
boolean isDigit = Character.isDigit(c);
864+
for (int i = offset + 1; i < source.length(); i++) {
865+
866+
c = source.charAt(i);
867+
if ((isDigit && !Character.isDigit(c)) || (!isDigit && Character.isDigit(c))) {
868+
break;
869+
}
870+
builder.append(c);
871+
}
872+
873+
return new Part(builder.toString(), isDigit);
874+
}
875+
876+
private static class Part implements Comparable<Part> {
877+
878+
private final String rawValue;
879+
private final Long longValue;
880+
881+
Part(String value, boolean isDigit) {
882+
883+
this.rawValue = value;
884+
this.longValue = isDigit ? Long.valueOf(value) : null;
885+
}
886+
887+
boolean isNumeric() {
888+
return longValue != null;
889+
}
890+
891+
int length() {
892+
return rawValue.length();
893+
}
894+
895+
/*
896+
* (non-Javadoc)
897+
* @see java.lang.Comparable#compareTo(java.lang.Object)
898+
*/
899+
@Override
900+
public int compareTo(Part that) {
901+
902+
if (this.isNumeric() && that.isNumeric()) {
903+
return this.longValue.compareTo(that.longValue);
904+
}
905+
906+
return this.rawValue.compareTo(that.rawValue);
907+
}
908+
}
909+
}
910+
813911
}

src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015 the original author or authors.
2+
* Copyright 2015-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.redis.core.convert;
1717

18+
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
1819
import static org.hamcrest.core.Is.*;
1920
import static org.hamcrest.core.IsCollectionContaining.*;
2021
import static org.hamcrest.core.IsInstanceOf.*;
@@ -308,7 +309,22 @@ public void readConvertsListOfSimplePropertiesCorrectly() {
308309
map.put("nicknames.[1]", "lews therin");
309310
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
310311

311-
assertThat(converter.read(Person.class, rdo).nicknames, hasItems("dragon reborn", "lews therin"));
312+
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "lews therin"));
313+
}
314+
315+
/**
316+
* @see DATAREDIS-425
317+
*/
318+
@Test
319+
public void readConvertsUnorderedListOfSimplePropertiesCorrectly() {
320+
321+
Map<String, String> map = new LinkedHashMap<String, String>();
322+
map.put("nicknames.[9]", "car'a'carn");
323+
map.put("nicknames.[10]", "lews therin");
324+
map.put("nicknames.[1]", "dragon reborn");
325+
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
326+
327+
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "car'a'carn", "lews therin"));
312328
}
313329

314330
/**
@@ -338,6 +354,7 @@ public void readListComplexPropertyCorrectly() {
338354
Map<String, String> map = new LinkedHashMap<String, String>();
339355
map.put("coworkers.[0].firstname", "mat");
340356
map.put("coworkers.[0].nicknames.[0]", "prince of the ravens");
357+
map.put("coworkers.[0].nicknames.[1]", "gambler");
341358
map.put("coworkers.[1].firstname", "perrin");
342359
map.put("coworkers.[1].address.city", "two rivers");
343360
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
@@ -348,6 +365,34 @@ public void readListComplexPropertyCorrectly() {
348365
assertThat(target.coworkers.get(0).firstname, is("mat"));
349366
assertThat(target.coworkers.get(0).nicknames, notNullValue());
350367
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
368+
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
369+
370+
assertThat(target.coworkers.get(1).firstname, is("perrin"));
371+
assertThat(target.coworkers.get(1).address.city, is("two rivers"));
372+
}
373+
374+
/**
375+
* @see DATAREDIS-425
376+
*/
377+
@Test
378+
public void readUnorderedListOfComplexPropertyCorrectly() {
379+
380+
Map<String, String> map = new LinkedHashMap<String, String>();
381+
map.put("coworkers.[10].firstname", "perrin");
382+
map.put("coworkers.[10].address.city", "two rivers");
383+
map.put("coworkers.[1].firstname", "mat");
384+
map.put("coworkers.[1].nicknames.[1]", "gambler");
385+
map.put("coworkers.[1].nicknames.[0]", "prince of the ravens");
386+
387+
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
388+
389+
Person target = converter.read(Person.class, rdo);
390+
391+
assertThat(target.coworkers, notNullValue());
392+
assertThat(target.coworkers.get(0).firstname, is("mat"));
393+
assertThat(target.coworkers.get(0).nicknames, notNullValue());
394+
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
395+
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
351396

352397
assertThat(target.coworkers.get(1).firstname, is("perrin"));
353398
assertThat(target.coworkers.get(1).address.city, is("two rivers"));
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.repository;
17+
18+
import static org.springframework.data.redis.connection.ClusterTestVariables.*;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.junit.ClassRule;
24+
import org.junit.runner.RunWith;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.ComponentScan;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.context.annotation.FilterType;
29+
import org.springframework.data.redis.connection.RedisClusterConfiguration;
30+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
31+
import org.springframework.data.redis.core.RedisTemplate;
32+
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
33+
import org.springframework.data.redis.test.util.RedisClusterRule;
34+
import org.springframework.test.context.ContextConfiguration;
35+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
36+
37+
/**
38+
* @author Christoph Strobl
39+
*/
40+
@RunWith(SpringJUnit4ClassRunner.class)
41+
@ContextConfiguration
42+
public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationTestBase {
43+
44+
static final List<String> CLUSTER_NODES = Arrays.asList(CLUSTER_NODE_1.asString(), CLUSTER_NODE_2.asString(),
45+
CLUSTER_NODE_3.asString());
46+
47+
/**
48+
* ONLY RUN WHEN CLUSTER AVAILABLE
49+
*/
50+
public static @ClassRule RedisClusterRule clusterRule = new RedisClusterRule();
51+
52+
@Configuration
53+
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
54+
keyspaceConfiguration = MyKeyspaceConfiguration.class,
55+
includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") })
56+
static class Config {
57+
58+
@Bean
59+
RedisTemplate<?, ?> redisTemplate() {
60+
61+
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(CLUSTER_NODES);
62+
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(clusterConfig);
63+
64+
connectionFactory.afterPropertiesSet();
65+
66+
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
67+
template.setConnectionFactory(connectionFactory);
68+
69+
return template;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)