Skip to content

Commit dd9db16

Browse files
DATAREDIS-425 - Normalize index paths for maps and lists.
Remove map key and list index values for secondary index structures. Paths like 'map.[key].property' are normalized to map.key.property. Same applies for lists 'list.[index].property' is converted to 'list.property'. To allow better control over index resolution the entire logic has been moved to IndexResolver now inspecting the whole type instead of single properties.
1 parent a161fd5 commit dd9db16

File tree

10 files changed

+858
-154
lines changed

10 files changed

+858
-154
lines changed

src/asciidoc/reference/redis-repositories.adoc

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void basicCrudOperations() {
8989
repo.delete(rand); <4>
9090
}
9191
----
92-
<1> Generates a new id if current values is `null` or reuses an already set id value and stores properties of type `Person` inside the Redis Hash with key with pattern `keyspace:id` in this case eg. `persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5`.
92+
<1> Generates a new id if current value is `null` or reuses an already set id value and stores properties of type `Person` inside the Redis Hash with key with pattern `keyspace:id` in this case eg. `persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5`.
9393
<2> Uses the provided id to retrieve the object stored at `keyspace:id`.
9494
<3> Counts the total number of entities available within the keyspace _persons_ defined by `@RedisHash` on `Person`.
9595
<4> Removes the key for the given object from Redis.
@@ -191,6 +191,28 @@ SADD persons:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e
191191
----
192192
====
193193

194+
Further more the programmatic setup allows to define indexes on map keys and list properties.
195+
196+
====
197+
[source,java]
198+
----
199+
@RedisHash("persons");
200+
public class Person {
201+
202+
// ... other properties omitted
203+
204+
Map<String,String> attributes; <1>
205+
Map<String Person> relatives; <2>
206+
List<Address> addresses; <3>
207+
}
208+
----
209+
<1> `SADD persons:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e`
210+
<2> `SADD persons:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e`
211+
<3> `SADD persons:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e`
212+
====
213+
214+
NOTE: Indexes will not be resolved on <<redis.repositories.references,References>>.
215+
194216
Same as with _keyspaces_ it is possible to configure indexes without the need of annotating the actual domain type.
195217

196218
.Index Setup via @EnableRedisRepositories
@@ -260,7 +282,7 @@ NOTE: The keyspace notification message listener will alter `notify-keyspace-eve
260282

261283
[[redis.repositories.mapping]]
262284
== Object to hash mapping
263-
The Redis Repository support persists Objects in Hashes. This requires an Object to Hash conversion which is done by a `RedisConverter`. The default implementation uses `Converter`s for mapping property values to and from Redis native `byte[]`.
285+
The Redis Repository support persists Objects in Hashes. This requires an Object to Hash conversion which is done by a `RedisConverter`. The default implementation uses `Converter` for mapping property values to and from Redis native `byte[]`.
264286

265287
Given the `Person` type from the previous sections the default mapping looks like the following:
266288

@@ -286,29 +308,39 @@ address.country = andor
286308
| Sample
287309
| Mapped Value
288310

289-
| Simple Type (eg. String)
311+
| Simple Type +
312+
(eg. String)
290313
| String firstname = "rand";
291314
| firstname = "rand"
292315

293-
| Complex Type (eg. Address)
316+
| Complex Type +
317+
(eg. Address)
294318
| Address adress = new Address("emond's field");
295319
| address.city = "emond's field"
296320

297-
| List of Simple Type
321+
| List +
322+
of Simple Type
298323
| List<String> nicknames = asList("dragon reborn", "lews therin");
299-
| nicknames.[0] = "dragon reborn", nicknames.[1] = "lews therin"
324+
| nicknames.[0] = "dragon reborn", +
325+
nicknames.[1] = "lews therin"
300326

301-
| Map of Simple Type
327+
| Map +
328+
of Simple Type
302329
| Map<String, String> atts = asMap({"eye-color", "grey"}, {"...
303-
| atts.[eye-color] = "grey", atts.[...
330+
| atts.[eye-color] = "grey", +
331+
atts.[hair-color] = "...
304332

305-
| List of Complex Type
333+
| List +
334+
of Complex Type
306335
| List<Address> addresses = asList(new Address("em...
307-
| addresses.[0].city = "emond's field", addresses.[1].city = "...
336+
| addresses.[0].city = "emond's field", +
337+
addresses.[1].city = "...
308338

309-
| Map of Complex Type
339+
| Map +
340+
of Complex Type
310341
| Map<String, Address> addresses = asMap({"home", new Address("em...
311-
| addresses.[home].city = "emond's field", addresses.[work].city = "...
342+
| addresses.[home].city = "emond's field", +
343+
addresses.[work].city = "...
312344
|===
313345

314346
Mapping behavior can be customized by registering the according `Converter`s in `CustomConversions`. Those Converters can take care of converting from/to a single `byte[]` as well as `Map<Sting,byte[]>` whereas the first one is suiteable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure, whereas the second options offers full control over the resulting hash.
@@ -416,12 +448,14 @@ _class = org.example.Person
416448
id = e2c7dcee-b8cd-4424-883e-736ce564363e
417449
firstname = rand
418450
lastname = al’thor
419-
mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56
451+
mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1>
420452
----
453+
<1> Reference stores the whole key (`keyspace:id`) of the referenced object.
421454
====
422455

423456

424-
WARNING: Referenced Objects are not subject of persisting changes when saving the referencing object. Please make sure to persist changes on referenced objects separately, since only the reference will be stored.
457+
WARNING: Referenced Objects are not subject of persisting changes when saving the referencing object. Please make sure to persist changes on referenced objects separately, since only the reference will be stored.
458+
Indexes set on properties of referenced types will not be resolved.
425459

426460
[[redis.repositories.queries]]
427461
== Query Methods

src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext
132132
Assert.notNull(mappingContext, "RedisMappingContext must not be null!");
133133

134134
MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, new IndexResolverImpl(
135-
mappingContext.getMappingConfiguration().getIndexConfiguration()), new ReferenceResolverImpl(this));
135+
mappingContext), new ReferenceResolverImpl(this));
136136
mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions);
137137
mappingConverter.afterPropertiesSet();
138138

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ public boolean isSimpleTargetType() {
479479
* @return
480480
*/
481481
private static boolean isRedisBasicType(Class<?> type) {
482-
return byte[].class.equals(type);
482+
return (byte[].class.equals(type) || Map.class.equals(type));
483483
}
484484
}
485485
}

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

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

18+
import java.util.Set;
19+
1820
import org.springframework.data.mapping.PersistentProperty;
21+
import org.springframework.data.util.TypeInformation;
1922

2023
/**
2124
* {@link IndexResolver} extracts secondary index structures to be applied on a given path, {@link PersistentProperty}
@@ -27,14 +30,12 @@
2730
public interface IndexResolver {
2831

2932
/**
30-
* Extracts a potential secondary index.
33+
* Resolves all indexes for given type information / value combination.
3134
*
32-
* @param keyspace must not be {@literal null}.
33-
* @param path can be {@literal null}.
34-
* @param property must not be {@literal null}.
35-
* @param value can be {@literal null}.
36-
* @return {@literal null} if no index could be resolved for given arguments.
35+
* @param typeInformation must not be {@literal null}.
36+
* @param value the actual value. Can be {@literal null}.
37+
* @return never {@literal null}.
3738
*/
38-
IndexedData resolveIndex(String keyspace, String path, PersistentProperty<?> property, Object value);
39+
Set<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value);
3940

4041
}

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

Lines changed: 135 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,24 @@
1515
*/
1616
package org.springframework.data.redis.core.convert;
1717

18+
import java.util.Collections;
19+
import java.util.LinkedHashSet;
20+
import java.util.Map;
21+
import java.util.Map.Entry;
22+
import java.util.Set;
23+
24+
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
1825
import org.springframework.data.mapping.PersistentProperty;
26+
import org.springframework.data.mapping.PersistentPropertyAccessor;
27+
import org.springframework.data.mapping.PropertyHandler;
1928
import org.springframework.data.redis.core.index.IndexConfiguration;
2029
import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting;
2130
import org.springframework.data.redis.core.index.Indexed;
31+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
32+
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
33+
import org.springframework.data.util.ClassTypeInformation;
34+
import org.springframework.data.util.TypeInformation;
35+
import org.springframework.util.Assert;
2236

2337
/**
2438
* {@link IndexResolver} implementation considering properties annotated with {@link Indexed} or paths set up in
@@ -30,36 +44,132 @@
3044
public class IndexResolverImpl implements IndexResolver {
3145

3246
private IndexConfiguration indexConfiguration;
47+
private RedisMappingContext mappingContext;
3348

3449
/**
3550
* Creates new {@link IndexResolverImpl} with empty {@link IndexConfiguration}.
3651
*/
3752
public IndexResolverImpl() {
38-
this(new IndexConfiguration());
53+
this(new RedisMappingContext());
3954
}
4055

4156
/**
4257
* Creates new {@link IndexResolverImpl} with given {@link IndexConfiguration}.
4358
*
44-
* @param indexConfiguration can be {@literal null} and will be defaulted to an empty {@link IndexConfiguration} if
45-
* so.
59+
* @param mapppingContext must not be {@literal null}.
4660
*/
47-
public IndexResolverImpl(IndexConfiguration indexConfiguration) {
48-
this.indexConfiguration = indexConfiguration != null ? indexConfiguration : new IndexConfiguration();
61+
public IndexResolverImpl(RedisMappingContext mappingContext) {
62+
63+
Assert.notNull(mappingContext, "MappingContext must not be null!");
64+
this.mappingContext = mappingContext;
65+
this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration();
4966
}
5067

5168
/*
5269
* (non-Javadoc)
53-
* @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndex(java.lang.String, java.lang.String, org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty, java.lang.Object)
70+
* @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object)
5471
*/
55-
@Override
56-
public IndexedData resolveIndex(String keyspace, String path, PersistentProperty<?> property, Object value) {
72+
public Set<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value) {
73+
return doResolveIndexesFor(mappingContext.getPersistentEntity(typeInformation).getKeySpace(), "", typeInformation,
74+
value);
75+
}
76+
77+
private Set<IndexedData> doResolveIndexesFor(final String keyspace, final String path,
78+
TypeInformation<?> typeInformation, Object value) {
79+
80+
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeInformation);
81+
82+
if (entity == null) {
83+
84+
IndexedData index = resolveIndex(keyspace, path, null, value);
85+
if (index != null) {
86+
return Collections.singleton(index);
87+
}
88+
return Collections.emptySet();
89+
}
90+
91+
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
92+
final Set<IndexedData> indexes = new LinkedHashSet<IndexedData>();
93+
94+
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
95+
96+
@Override
97+
public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) {
98+
99+
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
100+
101+
Object propertyValue = accessor.getProperty(persistentProperty);
102+
103+
if (propertyValue != null) {
104+
105+
TypeInformation<?> typeHint = persistentProperty.isMap() ? persistentProperty.getTypeInformation()
106+
.getMapValueType() : persistentProperty.getTypeInformation().getActualType();
107+
108+
IndexedData index = resolveIndex(keyspace, currentPath, persistentProperty, propertyValue);
109+
110+
if (index != null) {
111+
indexes.add(index);
112+
}
113+
114+
if (persistentProperty.isMap()) {
115+
116+
for (Entry<?, ?> entry : ((Map<?, ?>) propertyValue).entrySet()) {
117+
118+
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue());
119+
indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(),
120+
typeToUse.getActualType(), entry.getValue()));
121+
}
122+
123+
} else if (persistentProperty.isCollectionLike()) {
124+
125+
for (Object listValue : (Iterable<?>) propertyValue) {
126+
127+
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, listValue);
128+
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), listValue));
129+
}
130+
}
131+
132+
else if (persistentProperty.isEntity()
133+
|| persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) {
134+
135+
typeHint = updateTypeHintForActualValue(typeHint, propertyValue);
136+
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), propertyValue));
137+
}
138+
}
139+
140+
}
141+
142+
private TypeInformation<?> updateTypeHintForActualValue(TypeInformation<?> typeHint, Object propertyValue) {
143+
144+
if (typeHint.equals(ClassTypeInformation.OBJECT) || typeHint.getClass().isInterface()) {
145+
try {
146+
typeHint = mappingContext.getPersistentEntity(propertyValue.getClass()).getTypeInformation();
147+
} catch (Exception e) {
148+
// ignore for cases where property value cannot be resolved as an entity, in that case the provided type
149+
// hint has to be sufficient
150+
}
151+
}
152+
return typeHint;
153+
}
154+
155+
});
156+
157+
return indexes;
158+
}
159+
160+
protected IndexedData resolveIndex(String keyspace, String propertyPath, PersistentProperty<?> property, Object value) {
161+
162+
if (value == null) {
163+
return null;
164+
}
165+
166+
String path = normalizeIndexPath(propertyPath, property);
57167

58168
if (indexConfiguration.hasIndexFor(keyspace, path)) {
59169
return new SimpleIndexedPropertyValue(keyspace, path, value);
60170
}
61171

62-
else if (property.isAnnotationPresent(Indexed.class)) {
172+
else if (property != null && property.isAnnotationPresent(Indexed.class)) {
63173

64174
Indexed indexed = property.findAnnotation(Indexed.class);
65175

@@ -75,4 +185,20 @@ else if (property.isAnnotationPresent(Indexed.class)) {
75185
}
76186
return null;
77187
}
188+
189+
private String normalizeIndexPath(String path, PersistentProperty<?> property) {
190+
191+
if (property == null) {
192+
return path;
193+
}
194+
195+
if (property.isMap()) {
196+
return path.replaceAll("\\[", "").replaceAll("\\]", "");
197+
}
198+
if (property.isCollectionLike()) {
199+
return path.replaceAll("\\[(\\p{Digit})*\\]", "").replaceAll("\\.\\.", ".");
200+
}
201+
202+
return path;
203+
}
78204
}

0 commit comments

Comments
 (0)