Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpringData Redis Repository with complex key

We try to use the Spring Data CrudRepository in our project to provide persistency for our domain objects.
For a start I chose REDIS as backend since in a first experiment with a CrudRepository<ExperimentDomainObject, String> it seemd, getting it running is easy.

When trying to put it in our production code, things got more complicated, because here our domain objects were not necesseriliy using a simple type as key so the repository was CrudRepository<TestObject, ObjectId>.

Now I got the exception:

No converter found capable of converting from type [...ObjectId] to type [byte[]]

Searching for this exception, this answer which led my to uneducated experimenting with the RedisTemplate configuration. (For my experiment I am using emdedded-redis)

My idea was, to provide a RedisTemplate<Object, Object> instead of RedisTemplate<String, Object> to allow using the Jackson2JsonRedisSerializer to do the work as keySerializer also.

Still, calling testRepository.save(testObject) fails.

Please see my code:

I have public fields and left out the imports for the brevity of this example. If they are required (to make this a MVCE) I will happily provide them. Just leave me a comment.

dependencies:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'redis.clients', name: "jedis", version: '2.9.0'
    implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
}

RedisConfiguration:

@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        final RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setDefaultSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setEnableDefaultSerializer(true);

        return template;
    }
}

TestObject

@RedisHash("test")
public class TestObject
{
    @Id public ObjectId testId;
    public String value;

    public TestObject(ObjectId id, String value)
    {
        this.testId = id;
        this.value = value; // In experiment this is: "magic"
    }
}

ObjectId

@EqualsAndHashCode
public class ObjectId {
    public String creator; // In experiment, this is "me"
    public String name;    // In experiment, this is "fool"
}

TestRepository

@Repository
public interface TestRepository extends CrudRepository<TestObject, ObjectId>
{
}

EmbeddedRedisConfiguration

@Configuration
public class EmbeddedRedisConfiguration
{
    private final redis.embedded.RedisServer redisServer;

    EmbeddedRedisConfiguration(RedisProperties redisProperties)
    {
        this.redisServer = new redis.embedded.RedisServer(redisProperties.getPort());
    }

    @PostConstruct
    public void init()
    {
        redisServer.start();
    }

    @PreDestroy
    public void shutdown()
    {
        redisServer.stop();
    }
}

Application:

@SpringBootApplication
public class ExperimentApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ExperimentApplication.class, args);
    }
}

Not the desired Answer:

Of course, I might introduce some special ID which is a simple datatype, e.g. a JSON-String which I build manually using jacksons ObjectMapper and then use a CrudRepository<TestObject, String>.

What I also tried in the meantime:

  • RedisTemplate<String, String>
  • RedisTemplate<String, Object>
  • Autowireing a RedisTemplate and setting its default serializer
  • Registering a Converter<ObjectId, byte[]> to
    • An autowired ConverterRegistry
    • An autowired GenericConversionService
      but apparently they have been the wrong ones.
like image 781
derM Avatar asked Oct 29 '19 11:10

derM


People also ask

How do I store items in Redis cache spring boot?

In spring boot you need to set spring. cache. type=redis and this would create your cache using Redis. @Autowired RedisTemplate<String, String> redisTemplate; redisTemplate.

Is RedisTemplate thread safe?

Once configured, the template is thread-safe and can be reused across multiple instances. RedisTemplate uses a Java-based serializer for most of its operations.

What is the use of @RedisHash?

@RedisHash can take as parameters a timeToLive of type Long, and a String value, which will override the default Redis key prefix (by default, the key prefix is the fully qualified name of the class plus a colon). Within the class, most of the usual Spring Data annotations are supported.

What is RedisConnectionFactory?

Provides a suitable connection for interacting with Redis. Returns: connection for interacting with Redis. Throws: IllegalStateException - if the connection factory requires initialization and the factory was not yet initialized.


1 Answers

Basically, the Redis repositories use the RedisKeyValueTemplate under the hood to store data as Key (Id) and Value pair. So your configuration of RedisTemplate will not work unless you directly use it.

So one way for you will be to use the RedistTemplate directly, something like this will work for you.

@Service
public class TestService {

    @Autowired
    private RedisTemplate redisTemplate;

    public void saveIt(TestObject testObject){
        ValueOperations<ObjectId, TestObject> values = redisTemplate.opsForValue();
        values.set(testObject.testId, testObject);
    }

}

So the above code will use your configuration and generate the string pair in the Redis using the Jackson as the mapper for both the key and the value.

But if you want to use the Redis Repositories via CrudRepository you need to create reading and writing converters for ObjectId from and to String and byte[] and register them as custom Redis conversions.

So let's create reading and writing converters for ObjectId <-> String

Reader

@Component
@ReadingConverter
@Slf4j
public class RedisReadingStringConverter implements Converter<String, ObjectId> {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public ObjectId convert(String source) {
        try {
            return objectMapper.readValue(source, ObjectId.class);
        } catch (IOException e) {
            log.warn("Error while converting to ObjectId.", e);
            throw new IllegalArgumentException("Can not convert to ObjectId");
        }
    }
}

Writer

@Component
@WritingConverter
@Slf4j
public class RedisWritingStringConverter implements Converter<ObjectId, String> {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convert(ObjectId source) {
        try {
            return objectMapper.writeValueAsString(source);
        } catch (JsonProcessingException e) {
            log.warn("Error while converting ObjectId to String.", e);
            throw new IllegalArgumentException("Can not convert ObjectId to String");
        }
    }
}

And the reading and writing converters for ObjectId <-> byte[]

Writer

@Component
@WritingConverter
public class RedisWritingByteConverter implements Converter<ObjectId, byte[]> {

    Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);

    @Override
    public byte[] convert(ObjectId source) {
        return jackson2JsonRedisSerializer.serialize(source);
    }
}

Reader

@Component
@ReadingConverter
public class RedisReadingByteConverter implements Converter<byte[], ObjectId> {

     Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);

    @Override
    public ObjectId convert(byte[] source) {
        return jackson2JsonRedisSerializer.deserialize(source);
    }
}

And last add the Redis custom conversations. Just put the code into the RedisConfiguration

@Bean
public RedisCustomConversions redisCustomConversions(RedisReadingByteConverter readingConverter,
                                                     RedisWritingByteConverter redisWritingConverter,
                                                     RedisWritingStringConverter redisWritingByteConverter,
                                                     RedisReadingStringConverter redisReadingByteConverter) {
    return new RedisCustomConversions(Arrays.asList(readingConverter, redisWritingConverter, redisWritingByteConverter, redisReadingByteConverter));
}

So now after the converters are created and registered as custom Redis Converters the RedisKeyValueTemplate can use them and your code should work as expected.

like image 119
Babl Avatar answered Oct 13 '22 06:10

Babl