Skip to content

Commit

Permalink
feature: Ability to modify/filter the ID portion of the Key (Redis Cl…
Browse files Browse the repository at this point in the history
…uster Hashtags) (resolves gh-454)
  • Loading branch information
bsbodden committed Jun 6, 2024
1 parent c4fedd7 commit 777a1cb
Show file tree
Hide file tree
Showing 16 changed files with 648 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.redis.om.spring.audit.EntityAuditor;
import com.redis.om.spring.convert.MappingRedisOMConverter;
import com.redis.om.spring.convert.RedisOMCustomConversions;
import com.redis.om.spring.id.IdentifierFilter;
import com.redis.om.spring.indexing.RediSearchIndexer;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.ops.search.SearchOperations;
Expand Down Expand Up @@ -486,4 +487,21 @@ void addFieldToRemove(byte[] field) {
fieldsToRemove.add(field);
}
}

/**
* Creates a new {@link byte[] key} using the given {@link String keyspace} and {@link String id}.
*
* @param keyspace {@link String name} of the Redis {@literal keyspace}.
* @param id {@link String} identifying the key.
* @return a {@link byte[]} constructed from the {@link String keyspace} and {@link String id}.
*/
public byte[] createKey(String keyspace, String id) {
// handle IdFilters
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(keyspace);
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id);
}
return toBytes(keyspace + ":" + id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.gson.reflect.TypeToken;
import com.redis.om.spring.audit.EntityAuditor;
import com.redis.om.spring.convert.RedisOMCustomConversions;
import com.redis.om.spring.id.IdentifierFilter;
import com.redis.om.spring.indexing.RediSearchIndexer;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.ops.json.JSONOperations;
Expand Down Expand Up @@ -41,7 +42,6 @@
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static com.redis.om.spring.util.ObjectUtils.getKey;
import static com.redis.om.spring.util.ObjectUtils.isPrimitiveOfType;

public class RedisJSONKeyValueAdapter extends RedisKeyValueAdapter {
Expand All @@ -63,14 +63,14 @@ public class RedisJSONKeyValueAdapter extends RedisKeyValueAdapter {
* @param redisOps must not be {@literal null}.
* @param rmo must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
* @param keyspaceToIndexMap must not be {@literal null}.
* @param indexer must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
public RedisJSONKeyValueAdapter( //
RedisOperations<?, ?> redisOps, //
RedisModulesOperations<?> rmo, //
RedisMappingContext mappingContext, //
RediSearchIndexer keyspaceToIndexMap, //
RediSearchIndexer indexer, //
GsonBuilder gsonBuilder, //
FeatureExtractor featureExtractor, //
RedisOMProperties redisOMProperties) {
Expand All @@ -79,7 +79,7 @@ public RedisJSONKeyValueAdapter( //
this.redisJSONOperations = modulesOperations.opsForJSON();
this.redisOperations = redisOps;
this.mappingContext = mappingContext;
this.indexer = keyspaceToIndexMap;
this.indexer = indexer;
this.auditor = new EntityAuditor(this.redisOperations);
this.gsonBuilder = gsonBuilder;
this.featureExtractor = featureExtractor;
Expand All @@ -98,7 +98,7 @@ public Object put(Object id, Object item, String keyspace) {
logger.debug(String.format("%s, %s, %s", id, item, keyspace));
@SuppressWarnings("unchecked") JSONOperations<String> ops = (JSONOperations<String>) redisJSONOperations;

String key = getKey(keyspace, id);
String key = createKeyAsString(keyspace, id);

processVersion(key, item);
auditor.processEntity(key, item);
Expand Down Expand Up @@ -129,7 +129,8 @@ public Object put(Object id, Object item, String keyspace) {
@Nullable
@Override
public <T> T get(Object id, String keyspace, Class<T> type) {
return get(getKey(keyspace, id), type);
String key = createKeyAsString(keyspace, id);
return get(key, type);
}

@Nullable
Expand Down Expand Up @@ -192,7 +193,8 @@ public <T> T delete(Object id, String keyspace, Class<T> type) {
@SuppressWarnings("unchecked") JSONOperations<String> ops = (JSONOperations<String>) redisJSONOperations;
T entity = get(id, keyspace, type);
if (entity != null) {
ops.del(getKey(keyspace, id), Path2.ROOT_PATH);
String key = createKeyAsString(keyspace, id);
ops.del(key, Path2.ROOT_PATH);
}

return entity;
Expand Down Expand Up @@ -243,8 +245,8 @@ public long count(String keyspace) {
*/
@Override
public boolean contains(Object id, String keyspace) {
Boolean exists = redisOperations.execute(
(RedisCallback<Boolean>) connection -> connection.keyCommands().exists(toBytes(getKey(keyspace, id))));
Boolean exists = redisOperations.execute((RedisCallback<Boolean>) connection -> connection.keyCommands()
.exists(toBytes(createKeyAsString(keyspace, id))));

return exists != null && exists;
}
Expand Down Expand Up @@ -360,4 +362,16 @@ private Number getEntityVersion(String key, String versionProperty) {
Long[] dbVersionArray = (Long[]) ops.get(key, type, Path2.of("$." + versionProperty));
return dbVersionArray != null ? dbVersionArray[0] : null;
}

public String createKeyAsString(String keyspace, Object id) {
String format = keyspace.endsWith(":") ? "%s%s" : "%s:%s";

// handle IdFilters
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(keyspace);
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id.toString());
}
return String.format(format, keyspace, id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.redis.om.spring.id;

public class IdAsHashTag implements IdentifierFilter<Object> {
@Override
public String filter(Object id) {
return "{" + id.toString() + "}";
}
}
18 changes: 18 additions & 0 deletions redis-om-spring/src/main/java/com/redis/om/spring/id/IdFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.redis.om.spring.id;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

/**
* Filters an identifier before reading/saving to Redis.
*
* @author Brian Sam-Bodden
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface IdFilter {
Class<? extends IdentifierFilter<?>> value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.redis.om.spring.id;

public interface IdentifierFilter<ID> {
String filter(ID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.redis.om.spring.annotations.*;
import com.redis.om.spring.id.IdFilter;
import com.redis.om.spring.id.IdentifierFilter;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.ops.search.SearchOperations;
import com.redis.om.spring.repository.query.QueryUtils;
Expand Down Expand Up @@ -32,6 +34,7 @@
import redis.clients.jedis.search.IndexDataType;
import redis.clients.jedis.search.schemafields.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Instant;
Expand All @@ -50,6 +53,7 @@ public class RediSearchIndexer {
private final Map<String, Class<?>> keyspaceToEntityClass = new ConcurrentHashMap<>();
private final Map<Class<?>, String> entityClassToKeySpace = new ConcurrentHashMap<>();
private final Map<Class<?>, String> entityClassToIndexName = new ConcurrentHashMap<>();
private final Map<Class<?>, IdentifierFilter<?>> entityClassToIdentifierFilter = new ConcurrentHashMap<>();
private final List<Class<?>> indexedEntityClasses = new ArrayList<>();
private final Map<Class<?>, List<SearchField>> entityClassToSchema = new ConcurrentHashMap<>();
private final Map<Pair<Class<?>, String>, String> entityClassFieldToAlias = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -220,6 +224,18 @@ public Class<?> getEntityClassForKeyspace(String keyspace) {
return keyspaceToEntityClass.get(getKeyspace(keyspace));
}

public Optional<IdentifierFilter<?>> getIdentifierFilterFor(Class<?> entityClass) {
if (entityClass != null && entityClassToIdentifierFilter.containsKey(entityClass)) {
return Optional.of(entityClassToIdentifierFilter.get(entityClass));
} else {
return Optional.empty();
}
}

public Optional<IdentifierFilter<?>> getIdentifierFilterFor(String keyspace) {
return getIdentifierFilterFor(keyspaceToEntityClass.get(keyspace.endsWith(":") ? keyspace : keyspace + ":"));
}

public String getKeyspaceForEntityClass(Class<?> entityClass) {
String keyspace = entityClassToKeySpace.get(entityClass);
if (keyspace == null) {
Expand Down Expand Up @@ -778,6 +794,21 @@ private Optional<SearchField> createIndexedFieldForIdField(Class<?> cl, List<Sch
indexAsTagFieldFor(maybeIdField.get(), isDocument, "", false, "|", Integer.MIN_VALUE, null)));
}
}

// register any @IdFilter annotation
if (idField.isAnnotationPresent(IdFilter.class)) {
IdFilter idFilter = idField.getAnnotation(IdFilter.class);
var identifierFilterClass = idFilter.value();
try {
var identifierFilter = (IdentifierFilter<?>) identifierFilterClass.getDeclaredConstructor().newInstance();
entityClassToIdentifierFilter.put(cl, identifierFilter);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException idFilterInstantiationException) {
logger.error(String.format("Could not instantiate IdFilter of type %s applied to class %s",
identifierFilterClass.getSimpleName(), cl), idFilterInstantiationException);
}
}

}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import redis.clients.jedis.json.Path2;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

@NoRepositoryBean
public interface RedisDocumentRepository<T, ID> extends KeyValueRepository<T, ID>, QueryByExampleExecutor<T> {
Expand All @@ -32,6 +33,8 @@ public interface RedisDocumentRepository<T, ID> extends KeyValueRepository<T, ID

Long getExpiration(ID id);

boolean setExpiration(ID id, Long expiration, TimeUnit timeUnit);

Iterable<T> bulkLoad(String file) throws IOException;

<S extends T> S update(S entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.QueryByExampleExecutor;

import java.util.concurrent.TimeUnit;

@NoRepositoryBean
public interface RedisEnhancedRepository<T, ID> extends KeyValueRepository<T, ID>, QueryByExampleExecutor<T> {

Expand All @@ -27,5 +29,7 @@ public interface RedisEnhancedRepository<T, ID> extends KeyValueRepository<T, ID

Long getExpiration(ID id);

boolean setExpiration(ID id, Long expiration, TimeUnit timeUnit);

String getKeyspace();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.redis.om.spring.RedisOMProperties;
import com.redis.om.spring.audit.EntityAuditor;
import com.redis.om.spring.convert.MappingRedisOMConverter;
import com.redis.om.spring.id.IdentifierFilter;
import com.redis.om.spring.id.ULIDIdentifierGenerator;
import com.redis.om.spring.indexing.RediSearchIndexer;
import com.redis.om.spring.metamodel.MetamodelField;
Expand Down Expand Up @@ -155,6 +156,12 @@ public Long getExpiration(ID id) {
return template.getExpire(getKey(id));
}

@Override
public boolean setExpiration(ID id, Long expiration, TimeUnit timeUnit) {
RedisTemplate<String, String> template = modulesOperations.template();
return Boolean.TRUE.equals(template.expire(getKey(id), expiration, timeUnit));
}

@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "The given Iterable of entities must not be null!");
Expand Down Expand Up @@ -241,10 +248,22 @@ public String getKeyspace() {
}

private String getKey(Object id) {
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(metadata.getJavaType());
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id.toString());
}
return getKeyspace() + id.toString();
}

public byte[] createKey(String keyspace, String id) {
// handle IdFilters
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(keyspace);
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id);
}

return this.mappingConverter.toBytes(keyspace.endsWith(":") ? keyspace + id : keyspace + ":" + id);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.redis.om.spring.RedisOMProperties;
import com.redis.om.spring.audit.EntityAuditor;
import com.redis.om.spring.convert.MappingRedisOMConverter;
import com.redis.om.spring.id.IdentifierFilter;
import com.redis.om.spring.id.ULIDIdentifierGenerator;
import com.redis.om.spring.indexing.RediSearchIndexer;
import com.redis.om.spring.metamodel.MetamodelField;
Expand Down Expand Up @@ -40,6 +41,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
Expand Down Expand Up @@ -144,6 +146,12 @@ public Long getExpiration(ID id) {
return template.getExpire(getKey(id));
}

@Override
public boolean setExpiration(ID id, Long expiration, TimeUnit timeUnit) {
RedisTemplate<String, String> template = modulesOperations.template();
return Boolean.TRUE.equals(template.expire(getKey(id), expiration, timeUnit));
}

/* (non-Javadoc)
*
* @see org.springframework.data.repository.CrudRepository#findAll() */
Expand Down Expand Up @@ -220,6 +228,11 @@ public String getKeyspace() {
}

private String getKey(Object id) {
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(metadata.getJavaType());
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id.toString());
}
return getKeyspace() + id.toString();
}

Expand Down Expand Up @@ -266,6 +279,13 @@ public <S extends T> List<S> saveAll(Iterable<S> entities) {
}

public byte[] createKey(String keyspace, String id) {
// handle IdFilters
var maybeIdentifierFilter = indexer.getIdentifierFilterFor(keyspace);
if (maybeIdentifierFilter.isPresent()) {
IdentifierFilter<String> filter = (IdentifierFilter<String>) maybeIdentifierFilter.get();
id = filter.filter(id);
}

return this.mappingConverter.toBytes(keyspace.endsWith(":") ? keyspace + id : keyspace + ":" + id);
}

Expand Down
Loading

0 comments on commit 777a1cb

Please sign in to comment.