Reputation: 834
I'm using Spring data Redis (version 2.1.1RELEASE) with the driver of lettuce (version 5.1.3RELEASE) I want to use this module: https://oss.redislabs.com/redisjson/ but the driver doesn't seem to support it.
I tried use the execute
method:
Object response = redisTemplate.execute((RedisCallback<String>) connection -> {
return (String) connection.execute("JSON.GET foo");
});
And got an error:
java.lang.IllegalArgumentException: No enum constant io.lettuce.core.protocol.CommandType.JSON.GET FOO
Is there a way to do that? How can I utilize Redis modules?
Upvotes: 1
Views: 911
Reputation: 731
With the current Spring data Redis (version 3.1.4) it is possible to execute custom commands template.execute((RedisCallback<byte[]>) c -> (byte[]) c.execute("JSON.GET", key.getBytes()))
Complete example:
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class TestService {
private final StringRedisTemplate template;
private final ObjectMapper mapper;
public <O> Optional<O> getObject(String key, Class<O> valueClass) {
return Optional.ofNullable(template.execute((RedisCallback<byte[]>) c -> (byte[]) c.execute("JSON.GET", key.getBytes())))
.map(v -> map(v, valueClass));
}
private <O> O map(byte[] value, Class<O> valueClass) {
try {
return mapper.readValue(value, valueClass);
} catch (IOException e) {
log.error("Can't deserialize the object: {}, with class {}: {}", value, valueClass, e.getMessage(), e);
return null;
}
}
}
Upvotes: 0
Reputation: 834
OK so I managed to do it like that:
First extend ProtocolKeyword
and replace all '_' chars with '.':
public interface ModuleCommand extends ProtocolKeyword {
@Override
default byte[] getBytes() {
return name().replaceAll("_", ".").getBytes(LettuceCharsets.ASCII);
}
}
Second, create an enum with the types of commands you wish to implement:
public enum JsonCommand implements ModuleCommand {
JSON_GET,
JSON_MGET,
JSON_SET,
JSON_ARRAPPEND,
JSON_DEL,
// other commands you wish to implement
;
}
Now here's the interesting part, I already integrated more modules so I has to generalize the execution part:
public abstract class ModuleAbstractManager {
protected ByteArrayCodec codec = ByteArrayCodec.INSTANCE;
@Autowired
private LettuceConnectionFactory connectionFactory;
protected <T> Optional<T> execute(String key, ModuleCommand jsonCommand, CommandOutput<byte[], byte[], T> output, String... args) {
List<byte[]> extraArgs = Arrays.stream(args)
.filter(arg -> !StringUtils.isEmpty(arg))
.map(String::getBytes)
.collect(Collectors.toList());
CommandArgs<byte[], byte[]> commandArgs = new CommandArgs<>(codec)
.addKey(key.getBytes())
.addValues(extraArgs);
LettuceConnection connection = (LettuceConnection) connectionFactory.getConnection();
try {
RedisFuture<T> future = connection.getNativeConnection().dispatch(jsonCommand, output, commandArgs);
return Optional.ofNullable(future.get());
}
catch (InterruptedException | ExecutionException e) {
return Optional.empty();
}
finally {
connection.close();
}
}
}
And finally the execution itself:
@Service
public class RedisJsonManager extends ModuleAbstractManager {
public static final String ROOT_PATH = ".";
public static final String OK_RESPONSE = "OK";
public static final String SET_IF_NOT_EXIST_FLAG = "NX";
public Optional<String> getValue(String key) {
return getValue(key, ROOT_PATH);
}
public Optional<String> getValue(String key, String path) {
if (StringUtils.isEmpty(path)) {
return Optional.empty();
}
return execute(key, JsonCommand.JSON_GET, new ValueOutput<>(codec), path)
.map(String::new);
}
public Optional<String> getValue(String key, List<String> multiPath) {
if (CollectionUtils.isEmpty(multiPath)) {
return Optional.empty();
}
String[] args = multiPath.toArray(new String[0]);
return execute(key, JsonCommand.JSON_GET, new ValueOutput<>(codec), args)
.map(String::new);
}
public boolean setValueIfNotExist(String key, String json) {
return setValue(key, json, ROOT_PATH, true);
}
public boolean setValueIfNotExist(String key, String json, String path) {
return setValue(key, json, path, true);
}
public boolean setValue(String key, String json) {
return setValue(key, json, ROOT_PATH, false);
}
public boolean setValue(String key, String json, String path) {
return setValue(key, json, path, false);
}
private boolean setValue(String key, String json, String path, boolean setIfNotExist) {
return execute(key, JsonCommand.JSON_SET, new StatusOutput<>(codec), path, json, setIfNotExist ? SET_IF_NOT_EXIST_FLAG : "")
.map(OK_RESPONSE::equals)
.orElse(false);
}
public Long addToArray(String key, String json) {
return addToArray(key, json, ROOT_PATH);
}
public Long addToArray(String key, String json, String path) {
return execute(key, JsonCommand.JSON_ARRAPPEND, new IntegerOutput<>(codec), path, json).orElse(0L);
}
public Long delete(String key) {
return delete(key, ROOT_PATH);
}
public Long delete(String key, String path) {
return execute(key, JsonCommand.JSON_DEL, new IntegerOutput<>(codec), path).orElse(0L);
}
}
Upvotes: 3