Skip to content

DATAREDIS-471 - Add support for partial updates. #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.0.BUILD-SNAPSHOT</version>
<version>1.8.0.DATAREDIS-471-SNAPSHOT</version>

<name>Spring Data Redis</name>

Expand Down
38 changes: 38 additions & 0 deletions src/main/asciidoc/reference/redis-repositories.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,44 @@ mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1>
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.
Indexes set on properties of referenced types will not be resolved.

[[redis.repositories.partial-updates]]
== Persisting Partial Updates
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` and `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 <<redis.repositories.expirations>>.
====

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.

[[redis.repositories.queries]]
== Queries and Query Methods
Query methods allow automatic derivation of simple finder queries from the method name.
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/org/springframework/data/redis/core/IndexWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.convert.IndexedData;
import org.springframework.data.redis.core.convert.RedisConverter;
import org.springframework.data.redis.core.convert.RemoveIndexedData;
import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.Assert;
Expand All @@ -32,7 +33,7 @@
* Redis. Depending on the type of {@link IndexedData} it uses eg. Sets with specific names to add actually referenced
* keys to. While doing so {@link IndexWriter} also keeps track of all indexes associated with the root types key, which
* allows to remove the root key from all indexes in case of deletion.
*
*
* @author Christoph Strobl
* @author Rob Winch
* @since 1.7
Expand All @@ -44,7 +45,7 @@ class IndexWriter {

/**
* Creates new {@link IndexWriter}.
*
*
* @param keyspace The key space to write index values to. Must not be {@literal null}.
* @param connection must not be {@literal null}.
* @param converter must not be {@literal null}.
Expand Down Expand Up @@ -104,7 +105,7 @@ private void createOrUpdateIndexes(Object key, Iterable<IndexedData> indexValues

/**
* Removes a key from all available indexes.
*
*
* @param key must not be {@literal null}.
*/
public void removeKeyFromIndexes(String keyspace, Object key) {
Expand Down Expand Up @@ -142,13 +143,14 @@ private void removeKeyFromExistingIndexes(byte[] key, Iterable<IndexedData> inde

/**
* Remove given key from all indexes matching {@link IndexedData#getIndexName()}:
*
*
* @param key
* @param indexedData
*/
protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) {

Assert.notNull(indexedData, "IndexedData must not be null!");

Set<byte[]> existingKeys = connection
.keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":*"));

Expand All @@ -168,7 +170,7 @@ private void addKeyToIndexes(byte[] key, Iterable<IndexedData> indexValues) {

/**
* Adds a given key to the index for {@link IndexedData#getIndexName()}.
*
*
* @param key must not be {@literal null}.
* @param indexedData must not be {@literal null}.
*/
Expand All @@ -177,7 +179,11 @@ protected void addKeyToIndex(byte[] key, IndexedData indexedData) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(indexedData, "IndexedData must not be null!");

if (indexedData instanceof SimpleIndexedPropertyValue) {
if (indexedData instanceof RemoveIndexedData) {
return;
}

else if (indexedData instanceof SimpleIndexedPropertyValue) {

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

Expand Down Expand Up @@ -212,7 +218,8 @@ private byte[] toBytes(Object source) {
}

throw new InvalidDataAccessApiUsageException(String.format(
"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?",
"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?",
source.getClass()));
}

Expand Down
238 changes: 238 additions & 0 deletions src/main/java/org/springframework/data/redis/core/PartialUpdate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* {@link PartialUpdate} allows to issue individual property updates without the need of rewriting the whole entity. It
* allows to define {@literal set}, {@literal delete} actions on existing objects while taking care of updating
* potential expiration times of the entity itself as well as index structures.
*
* @author Christoph Strobl
* @param <T>
* @since 1.8
*/
public class PartialUpdate<T> {

private final Object id;
private final Class<T> target;
private final T value;
private boolean refreshTtl = false;

private final List<PropertyUpdate> propertyUpdates = new ArrayList<PropertyUpdate>();

private PartialUpdate(Object id, Class<T> target, T value, boolean refreshTtl, List<PropertyUpdate> propertyUpdates) {

this.id = id;
this.target = target;
this.value = value;
this.refreshTtl = refreshTtl;
this.propertyUpdates.addAll(propertyUpdates);
}

/**
* Create new {@link PartialUpdate} for given id and type.
*
* @param id must not be {@literal null}.
* @param targetType must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
public PartialUpdate(Object id, Class<T> targetType) {

Assert.notNull(id, "Id must not be null!");
Assert.notNull(targetType, "TargetType must not be null!");

this.id = id;
this.target = (Class<T>) ClassUtils.getUserClass(targetType);
this.value = null;
}

/**
* Create new {@link PartialUpdate} for given id and object.
*
* @param id must not be {@literal null}.
* @param value must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
public PartialUpdate(Object id, T value) {

Assert.notNull(id, "Id must not be null!");
Assert.notNull(value, "Value must not be null!");

this.id = id;
this.target = (Class<T>) ClassUtils.getUserClass(value.getClass());
this.value = value;
}

/**
* Create new {@link PartialUpdate} for given id and type.
*
* @param id must not be {@literal null}.
* @param targetType must not be {@literal null}.
*/
public static <S> PartialUpdate<S> newPartialUpdate(Object id, Class<S> targetType) {
return new PartialUpdate<S>(id, targetType);
}

/**
* @return can be {@literal null}.
*/
public T getValue() {
return value;
}

/**
* Set the value of a simple or complex {@literal value} reachable via given {@literal path}.
*
* @param path must not be {@literal null}.
* @param value must not be {@literal null}. If you want to remove a value use {@link #del(String)}.
* @return a new {@link PartialUpdate}.
*/
public PartialUpdate<T> set(String path, Object value) {

Assert.hasText(path, "Path to set must not be null or empty!");

PartialUpdate<T> update = new PartialUpdate<T>(this.id, this.target, this.value, this.refreshTtl,
this.propertyUpdates);
update.propertyUpdates.add(new PropertyUpdate(UpdateCommand.SET, path, value));

return update;
}

/**
* Remove the value reachable via given {@literal path}.
*
* @param path path must not be {@literal null}.
* @return a new {@link PartialUpdate}.
*/
public PartialUpdate<T> del(String path) {

Assert.hasText(path, "Path to remove must not be null or empty!");

PartialUpdate<T> update = new PartialUpdate<T>(this.id, this.target, this.value, this.refreshTtl,
this.propertyUpdates);
update.propertyUpdates.add(new PropertyUpdate(UpdateCommand.DEL, path));

return update;
}

/**
* Get the target type.
*
* @return never {@literal null}.
*/
public Class<T> getTarget() {
return target;
}

/**
* Get the id of the element to update.
*
* @return never {@literal null}.
*/
public Object getId() {
return id;
}

/**
* Get the list of individual property updates.
*
* @return never {@literal null}.
*/
public List<PropertyUpdate> getPropertyUpdates() {
return Collections.unmodifiableList(propertyUpdates);
}

/**
* @return true if expiration time of target should be updated.
*/
public boolean isRefreshTtl() {
return refreshTtl;
}

/**
* Set indicator for updating expiration time of target.
*
* @param refreshTtl
* @return a new {@link PartialUpdate}.
*/
public PartialUpdate<T> refreshTtl(boolean refreshTtl) {
return new PartialUpdate<T>(this.id, this.target, this.value, refreshTtl, this.propertyUpdates);
}

/**
* @author Christoph Strobl
* @since 1.8
*/
public static class PropertyUpdate {

private final UpdateCommand cmd;
private final String propertyPath;
private final Object value;

private PropertyUpdate(UpdateCommand cmd, String propertyPath) {
this(cmd, propertyPath, null);
}

private PropertyUpdate(UpdateCommand cmd, String propertyPath, Object value) {

this.cmd = cmd;
this.propertyPath = propertyPath;
this.value = value;
}

/**
* Get the associated {@link UpdateCommand}.
*
* @return never {@literal null}.
*/
public UpdateCommand getCmd() {
return cmd;
}

/**
* Get the target path.
*
* @return never {@literal null}.
*/
public String getPropertyPath() {
return propertyPath;
}

/**
* Get the value to set.
*
* @return can be {@literal null}.
*/
public Object getValue() {
return value;
}
}

/**
* @author Christoph Strobl
* @since 1.8
*/
public static enum UpdateCommand {
SET, DEL
}
}
Loading