Skip to content

Commit 68870ad

Browse files
DATAREDIS-471 - Add support for partial updates.
We now allow partial update of domain types via PartialUpdate. The according expiration times and secondary index structures are updated accordingly. In some cases it is not necessary to load and rewrite the entire entity just to set a new value within it. A session timestamp for last active time might be such a scenario where you just want to alter one property. `PartialUpdate` allows to define `set`, `delete` actions on existing objects while taking care of updating potential expiration times of the entity itself as well as index structures. .Sample Partial Update ==== [source,java] ---- PartialUpdate<Person> update = new PartialUpdate<Person>("e2c7dcee", Person.class) .set("firstname", "mat") <1> .set("address.city", "emond's field") <2> .del("age"); <3> template.update(update); update = new PartialUpdate<Person>("e2c7dcee", Person.class) .set("address", new Address("caemlyn", "andor")) <4> .set("attributes", singletonMap("eye-color", "grey")); <5> template.update(update); update = new PartialUpdate<Person>("e2c7dcee", Person.class) .refreshTtl(true); <6> .set("expiration", 1000); template.update(update); ---- <1> Set the simple property _firstname_ to _mat_ <2> Set the simple property _address.city_ to _emond's field_ without having to pass in the entire object. This does not work when a custom conversion is registered. <3> Remove the property _age_. <4> Set complex property _address_. <5> Set a map/collection of values removes the previously existing map/collection and replaces the values with the given ones. <6> Automatically update the server expiration time when altering time to live. ==== NOTE: Updating complex objects as well as map/collection structures requires further interaction with Redis to determine existing values which means that it might turn out that rewriting the entire entity might be faster.
1 parent 4a0f6d5 commit 68870ad

20 files changed

+2032
-69
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,44 @@ mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1>
487487
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.
488488
Indexes set on properties of referenced types will not be resolved.
489489

490+
[[redis.repositories.partial-updates]]
491+
== Persisting Partial Updates
492+
In some cases it is not necessary to load and rewrite the entire entity just to set a new value within it. A session timestamp for last active time might be such a scenario where you just want to alter one property.
493+
`PartialUpdate` allows to define `set`, `delete` actions on existing objects while taking care of updating potential expiration times of the entity itself as well as index structures.
494+
495+
.Sample Partial Update
496+
====
497+
[source,java]
498+
----
499+
PartialUpdate<Person> update = new PartialUpdate<Person>("e2c7dcee", Person.class)
500+
.set("firstname", "mat") <1>
501+
.set("address.city", "emond's field") <2>
502+
.del("age"); <3>
503+
504+
template.update(update);
505+
506+
update = new PartialUpdate<Person>("e2c7dcee", Person.class)
507+
.set("address", new Address("caemlyn", "andor")) <4>
508+
.set("attributes", singletonMap("eye-color", "grey")); <5>
509+
510+
template.update(update);
511+
512+
update = new PartialUpdate<Person>("e2c7dcee", Person.class)
513+
.refreshTtl(true); <6>
514+
.set("expiration", 1000);
515+
516+
template.update(update);
517+
----
518+
<1> Set the simple property _firstname_ to _mat_
519+
<2> Set the simple property _address.city_ to _emond's field_ without having to pass in the entire object. This does not work when a custom conversion is registered.
520+
<3> Remove the property _age_.
521+
<4> Set complex property _address_.
522+
<5> Set a map/collection of values removes the previously existing map/collection and replaces the values with the given ones.
523+
<6> Automatically update the server expiration time when altering <<redis.repositories.expirations>>.
524+
====
525+
526+
NOTE: Updating complex objects as well as map/collection structures requires further interaction with Redis to determine existing values which means that it might turn out that rewriting the entire entity might be faster.
527+
490528
[[redis.repositories.queries]]
491529
== Queries and Query Methods
492530
Query methods allow automatic derivation of simple finder queries from the method name.

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.redis.connection.RedisConnection;
2222
import org.springframework.data.redis.core.convert.IndexedData;
2323
import org.springframework.data.redis.core.convert.RedisConverter;
24+
import org.springframework.data.redis.core.convert.RemoveIndexedData;
2425
import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue;
2526
import org.springframework.data.redis.util.ByteUtils;
2627
import org.springframework.util.Assert;
@@ -123,8 +124,8 @@ private void removeKeyFromExistingIndexes(byte[] key, Iterable<IndexedData> inde
123124
protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) {
124125

125126
Assert.notNull(indexedData, "IndexedData must not be null!");
126-
Set<byte[]> existingKeys = connection.keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName()
127-
+ ":*"));
127+
Set<byte[]> existingKeys = connection
128+
.keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":*"));
128129

129130
if (!CollectionUtils.isEmpty(existingKeys)) {
130131
for (byte[] existingKey : existingKeys) {
@@ -151,7 +152,11 @@ protected void addKeyToIndex(byte[] key, IndexedData indexedData) {
151152
Assert.notNull(key, "Key must not be null!");
152153
Assert.notNull(indexedData, "IndexedData must not be null!");
153154

154-
if (indexedData instanceof SimpleIndexedPropertyValue) {
155+
if (indexedData instanceof RemoveIndexedData) {
156+
return;
157+
}
158+
159+
else if (indexedData instanceof SimpleIndexedPropertyValue) {
155160

156161
Object value = ((SimpleIndexedPropertyValue) indexedData).getValue();
157162

@@ -166,8 +171,8 @@ protected void addKeyToIndex(byte[] key, IndexedData indexedData) {
166171
// keep track of indexes used for the object
167172
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
168173
} else {
169-
throw new IllegalArgumentException(String.format("Cannot write index data for unknown index type %s",
170-
indexedData.getClass()));
174+
throw new IllegalArgumentException(
175+
String.format("Cannot write index data for unknown index type %s", indexedData.getClass()));
171176
}
172177
}
173178

@@ -185,10 +190,8 @@ private byte[] toBytes(Object source) {
185190
return converter.getConversionService().convert(source, byte[].class);
186191
}
187192

188-
throw new InvalidDataAccessApiUsageException(
189-
String
190-
.format(
191-
"Cannot convert %s to binary representation for index key generation. Are you missing a Converter? Did you register a non PathBasedRedisIndexDefinition that might apply to a complex type?",
192-
source.getClass()));
193+
throw new InvalidDataAccessApiUsageException(String.format(
194+
"Cannot convert %s to binary representation for index key generation. Are you missing a Converter? Did you register a non PathBasedRedisIndexDefinition that might apply to a complex type?",
195+
source.getClass()));
193196
}
194197
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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.core;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import org.springframework.util.Assert;
23+
import org.springframework.util.ClassUtils;
24+
25+
/**
26+
* {@link PartialUpdate} allows to issue individual property updates without the need of rewriting the whole entity. It
27+
* allows to define {@literal set}, {@literal delete} actions on existing objects while taking care of updating
28+
* potential expiration times of the entity itself as well as index structures.
29+
*
30+
* @author Christoph Strobl
31+
* @param <T>
32+
* @since 1.8
33+
*/
34+
public class PartialUpdate<T> {
35+
36+
private final Object id;
37+
private final Class<T> target;
38+
private final T value;
39+
private boolean refreshTtl = false;
40+
41+
private final List<PropertyUpdate> propertyUpdates = new ArrayList<PropertyUpdate>();
42+
43+
private PartialUpdate(Object id, Class<T> target, T value, boolean refreshTtl, List<PropertyUpdate> propertyUpdates) {
44+
45+
this.id = id;
46+
this.target = target;
47+
this.value = value;
48+
this.refreshTtl = refreshTtl;
49+
this.propertyUpdates.addAll(propertyUpdates);
50+
}
51+
52+
/**
53+
* Create new {@link PartialUpdate} for given id and type.
54+
*
55+
* @param id must not be {@literal null}.
56+
* @param targetType must not be {@literal null}.
57+
*/
58+
@SuppressWarnings("unchecked")
59+
public PartialUpdate(Object id, Class<T> targetType) {
60+
61+
Assert.notNull(id, "Id must not be null!");
62+
Assert.notNull(targetType, "TargetType must not be null!");
63+
64+
this.id = id;
65+
this.target = (Class<T>) ClassUtils.getUserClass(targetType);
66+
this.value = null;
67+
}
68+
69+
/**
70+
* Create new {@link PartialUpdate} for given id and object.
71+
*
72+
* @param id must not be {@literal null}.
73+
* @param value must not be {@literal null}.
74+
*/
75+
@SuppressWarnings("unchecked")
76+
public PartialUpdate(Object id, T value) {
77+
78+
Assert.notNull(id, "Id must not be null!");
79+
Assert.notNull(value, "Value must not be null!");
80+
81+
this.id = id;
82+
this.target = (Class<T>) ClassUtils.getUserClass(value.getClass());
83+
this.value = value;
84+
}
85+
86+
/**
87+
* Create new {@link PartialUpdate} for given id and type.
88+
*
89+
* @param id must not be {@literal null}.
90+
* @param targetType must not be {@literal null}.
91+
*/
92+
public static <S> PartialUpdate<S> newPartialUpdate(Object id, Class<S> targetType) {
93+
return new PartialUpdate<S>(id, targetType);
94+
}
95+
96+
/**
97+
* @return can be {@literal null}.
98+
*/
99+
public T getValue() {
100+
return value;
101+
}
102+
103+
/**
104+
* Set the value of a simple or complex {@literal value} reachable via given {@literal path}.
105+
*
106+
* @param path must not be {@literal null}.
107+
* @param value must not be {@literal null}. If you want to remove a value use {@link #del(String)}.
108+
* @return a new {@link PartialUpdate}.
109+
*/
110+
public PartialUpdate<T> set(String path, Object value) {
111+
112+
Assert.hasText(path, "Path to set must not be null or empty!");
113+
114+
PartialUpdate<T> update = new PartialUpdate<T>(this.id, this.target, this.value, this.refreshTtl,
115+
this.propertyUpdates);
116+
update.propertyUpdates.add(new PropertyUpdate(UpdateCommand.SET, path, value));
117+
return update;
118+
}
119+
120+
/**
121+
* Remove the value reachable via given {@literal path}.
122+
*
123+
* @param path path must not be {@literal null}.
124+
* @return a new {@link PartialUpdate}.
125+
*/
126+
public PartialUpdate<T> del(String path) {
127+
128+
Assert.hasText(path, "Path to remove must not be null or empty!");
129+
130+
PartialUpdate<T> update = new PartialUpdate<T>(this.id, this.target, this.value, this.refreshTtl,
131+
this.propertyUpdates);
132+
update.propertyUpdates.add(new PropertyUpdate(UpdateCommand.DEL, path));
133+
return update;
134+
}
135+
136+
/**
137+
* Get the target type.
138+
*
139+
* @return never {@literal null}.
140+
*/
141+
public Class<T> getTarget() {
142+
return target;
143+
}
144+
145+
/**
146+
* Get the id of the element to update.
147+
*
148+
* @return never {@literal null}.
149+
*/
150+
public Object getId() {
151+
return id;
152+
}
153+
154+
/**
155+
* Get the list of individual property updates.
156+
*
157+
* @return never {@literal null}.
158+
*/
159+
public List<PropertyUpdate> getPropertyUpdates() {
160+
return Collections.unmodifiableList(propertyUpdates);
161+
}
162+
163+
/**
164+
* @return true if expiration time of target should be updated.
165+
*/
166+
public boolean isRefreshTtl() {
167+
return refreshTtl;
168+
}
169+
170+
/**
171+
* Set indicator for updating expiration time of target.
172+
*
173+
* @param refreshTtl
174+
* @return a new {@link PartialUpdate}.
175+
*/
176+
public PartialUpdate<T> refreshTtl(boolean refreshTtl) {
177+
return new PartialUpdate<T>(this.id, this.target, this.value, refreshTtl, this.propertyUpdates);
178+
}
179+
180+
/**
181+
* @author Christoph Strobl
182+
* @since 1.8
183+
*/
184+
public static class PropertyUpdate {
185+
186+
private final UpdateCommand cmd;
187+
private final String propertyPath;
188+
private final Object value;
189+
190+
private PropertyUpdate(UpdateCommand cmd, String propertyPath) {
191+
this(cmd, propertyPath, null);
192+
}
193+
194+
private PropertyUpdate(UpdateCommand cmd, String propertyPath, Object value) {
195+
196+
this.cmd = cmd;
197+
this.propertyPath = propertyPath;
198+
this.value = value;
199+
}
200+
201+
/**
202+
* Get the associated {@link UpdateCommand}.
203+
*
204+
* @return never {@literal null}.
205+
*/
206+
public UpdateCommand getCmd() {
207+
return cmd;
208+
}
209+
210+
/**
211+
* Get the target path.
212+
*
213+
* @return never {@literal null}.
214+
*/
215+
public String getPropertyPath() {
216+
return propertyPath;
217+
}
218+
219+
/**
220+
* Get the value to set.
221+
*
222+
* @return can be {@literal null}.
223+
*/
224+
public Object getValue() {
225+
return value;
226+
}
227+
}
228+
229+
/**
230+
* @author Christoph Strobl
231+
* @since 1.8
232+
*/
233+
public static enum UpdateCommand {
234+
SET, DEL
235+
}
236+
}

0 commit comments

Comments
 (0)