Redis with Spring Boot

Configuration

Dependency

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

Specify the redis instance host/port

spring.redis.host=foohost
spring.redis.port=6379

Enable Redis and set the configuration

import java.util.Collections;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@EnableRedisRepositories
@Configuration
public class RedisConfiguration {
 
   // Optional : allow to define the key prefix for classes annotated with @RedisHash (instead of the simple class name as default)
   public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
 
      @Override
      protected Iterable<KeyspaceSettings> initialConfiguration() {
         return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
      }
   }
 
   @Bean
   RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory rcf) {
 
      RedisTemplate<String, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(rcf);
      template.setDefaultSerializer(new StringRedisSerializer());
 
      return template;
   }
}

Multiple API to interact with Redis

RedisTemplate

RedisTemplate and its corresponding package, org.springframework.data.redis.core is the central class of the Redis module, due to its rich feature set. The template offers a high-level abstraction for Redis interactions. While RedisConnection offers low-level methods that accept and return binary values (byte arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details.

Operational views :

Table 2. Operational views
InterfaceDescription

Key Type Operations

GeoOperations

Redis geospatial operations, such as GEOADD, GEORADIUS,…​

HashOperations

Redis hash operations

HyperLogLogOperations

Redis HyperLogLog operations, such as PFADD, PFCOUNT,…​

ListOperations

Redis list operations

SetOperations

Redis set operations

ValueOperations

Redis string (or value) operations

ZSetOperations

Redis zset (or sorted set) operations

Key Bound Operations

BoundGeoOperations

Redis key bound geospatial operations

BoundHashOperations

Redis hash key bound operations

BoundKeyOperations

Redis key bound operations

BoundListOperations

Redis list key bound operations

BoundSetOperations

Redis set key bound operations

BoundValueOperations

Redis string (or value) key bound operations

BoundZSetOperations

Redis zset (or sorted set) key bound operations

Hash mapping

The idea is to map an object of a custom class into a Redis hash.
Several ways :
– Direct mapping, by using HashOperations and a serializer
– Using Redis Repositories
– Using HashMapper and HashOperations

Hash mapping with Redis Repository

Constructor rules for model

If there’s a no-argument constructor, it will be used. Other constructors will be ignored.
If there’s a single constructor taking arguments, it will be used.
If there are multiple constructors taking arguments, the one to be used by Spring Data will have to be annotated with @PersistenceConstructor.

Person Model

package davidxxx;
 
import org.springframework.data.redis.core.RedisHash;
 
// value attr is optional. It allows to specific the prefix key (alternative to the config way seen above)
// timeToLive attr is optional. Expressed in seconds.
@RedisHash(value = "people", timeToLive = 86400) // 1 days
public class Person {
 
  @org.springframework.data.annotation.Id
  private Long id;
  private String firstname;
  private String lastname;
 
  public Person(Long id, String firstname, String lastname) {
    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
  }
 
  public Long getId() {
    return id;
  }
 
  public String getFirstname() {
    return firstname;
  }
 
  public String getLastname() {
    return lastname;
  }
 
  @Override public String toString() {
    return "Person{" +
           "id=" + id +
           ", firstname='" + firstname + '\'' +
           ", lastname='" + lastname + '\'' +
           '}';
  }
}

Person Repository

import Person;
import org.springframework.data.repository.CrudRepository;
 
public interface PersonRepository extends CrudRepository<Person, Long> {
 
}

Controller

package davidxxx;
 
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.spriangframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
 
import java.net.URI;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
 
@RequestMapping("person")
@RestController
public class PersonController {
 
  private PersonRepository personRepository;
 
  public PersonController(PersonRepository personRepository) {
    this.personRepository = personRepository;
  }
 
  @PostMapping
  public ResponseEntity<String> addPerson(@RequestBody Person person) throws SQLException {
    Person saved = personRepository.save(person);
    return ResponseEntity.created(URI.create("person/" + saved.getId())).body("");
  }
 
  @GetMapping(value = "/{id}", consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public ResponseEntity<Person> getPerson(@PathVariable("id") Long id) {
    return personRepository.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
  }
 
  @GetMapping(consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public List<Person> getAllPerson() {
    return StreamSupport
        .stream(personRepository.findAll().spliterator(), false)
        .collect(Collectors.toList());
  }
 
  @DeleteMapping(consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
  public ResponseEntity<Void> deleteAllPerson() {
    personRepository.deleteAll();
    return ResponseEntity.ok().build();
  }
 
}

Test with curls

# add 2 person
curl -v -X POST  -H "Content-Type: application/json"   --data '{"firstname":"david","lastname":"xxx"}'  "localhost:8090/person"
curl -v -X POST  -H "Content-Type: application/json"   --data '{"firstname":"Franck","lastname":"Castle"}'  "localhost:8090/person"
 
# find each  person  since the returned id in the URI
curl -v -X GET -H "Content-Type: text/plain"   "localhost:8090/person/8667656159371026199" 
->
{"id":8667656159371026199,"firstname":"david","lastname":"xxx"}
 
curl -v -X GET -H "Content-Type: text/plain"   "localhost:8090/person/-5700280251891896039"
->
{"id":-5700280251891896039,"firstname":"Franck","lastname":"Castle"}
 
# find all person
curl -v -X GET -H "Content-Type: text/plain"   "localhost:8090/person"
->
[{"id":8667656159371026199,"firstname":"david","lastname":"xxx"},{"id":-5700280251891896039,"firstname":"Franck","lastname":"Castle"}]
 
# delete person
curl -v -X DELETE -H "Content-Type: text/plain"   "localhost:8090/person"

Redis Repository constraints

Important document :
https://github.com/spring-projects/spring-data-redis/blob/master/src/main/asciidoc/reference/redis-repositories.adoc

Constraint on collection fields in a RedisHash class

Indeed, we cannot declare any Collection fields in a RedisHash class.
Due to the flat representation structure, Map keys need to be simple types, such as String or Number.
And map field cannot have as values another collection for the same reason.
While we could still mitigate the two issues.

How to allow collection values in a map ?

By default, we may encounter a weird behavior.
For example, with a Map<String, List<String>> map field, the serialization in redis may store some weird value in the list instead of the real content.
To overcome that, we could define a reader and writer converter for the object to store.

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *