Introduction to Spring Data Redis with Repository

Sudeep Moothedath
8 min readAug 25, 2019

Spring Data Redis. In the past blogs, I have covered advanced usage of Spring Boot + Redis using Jedis and using Spring Data Redis’ Template.

Someone recently asked me a question about the usage of Spring Data Redis with Spring’s Repositories. This made me to look for some reference documentations.

Spring’s official documentation has some details about Getting Started. The home page gives a quick example. As I went through these documents and tried to execute the sample code, I found that there was little description in those documents on the need for certain annotations available in the examples.

So, I decided to give my own take on the usage of Spring Data Redis with Repository; including explanations for some of the annotations used in the code.

The completed code is available here in my Git page.

Spring with Redis

Getting Started

Initialize

Let’s try to create a project that’ll allow us to do CRUD operations on a Person class defined in the Spring’s documentation, using a Repository.

The easiest way to get started is to use Spring Initializer. Select Spring Web Starter and Spring Data Redis (Access + Driver) as dependencies and Download the project. My sample code was generated using Gradle and Spring Boot 2.1.6 version. The current version as of publishing this post is 2.1.7.

Setup

Once the project was opened in an IDE (I use IntelliJ), the build.gradle file looks like below

plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.sudeep.redis'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Under the test, a J Unit test case for the Application class is autogenerated with a single test case contextLoads. Run this test case in IntelliJ to verify whether Spring Boot is able to load the application context.

Entities

Once the workspace is all set, Add the Person and Address classes. Our aim is to manipulate the Person entity through a Repository.

package com.sudeepnm.redis.introspringdataredis;

public class Person {
private String id;
private String firstname;
private String lastname;
private Address address;
}
package com.sudeepnm.redis.introspringdataredis;

public class Address {
private String street;
private String city;
private String state;
private String zipcode;
}

I’m skipping the details about test cases as you can refer to the code in Git.

Enhance the entities with Lombok

In order to provide accessors and constructor for the Person and Address beans, I used Lombok. This can be added to the build.gradle with the below line

implementation group: ‘org.projectlombok’, name: ‘lombok’, version: ‘1.18.8’

Added the below 3 annotations in both the beans.

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@AllArgsConstructor
@Getter
@Builder

Lombok creates an all argument constructor and a getter method for each element in the class.

I am using IntelliJ. In order to facilitate the generation of the code, Enable Annotation Processing option has to be selected under IntelliJ IDEA -> Preferences -> Build, Execution, Deployment -> Compiler -> Annotation Processors

Check Enable Annotation Processing

Ensure that the Lombok plugin is installed. Follow the steps here, if you need any help.

Run the contextLoads test case once again and it should run successfully.

Repository Setup

Next, create a PersonRepository which extends Spring Data’s CrudRepository. The CrudRepository needs a Type and Key. Since we are looking to manipulate Person with the repository, give the type as <Person, String>

Run the test case again and it should still run fine, since adding of these classes made no difference to the context load. Next, add @Repositoryon the PersonRepository to register it as a Repository. Even though it’s made as a Repository, Spring doesn’t have any information about the type of repository. In order to provide this, we need to add a configuration.

Create Configuration class and annotate it as @Configuration. Spring needs a RedisConnectionFactory and a RedisTemplate to connect with Redis. In this example, I used Jedis.

@org.springframework.context.annotation.Configuration
public class Configuration {

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> template = new RedisTemplate<>();
return template;
}
}

Add the redis url to application.yml(The generated project comes with application.properties. Since I prefer application.yml, I added it separately)

spring:
application:
name: intro-spring-data-redis
redis:
url: redis://localhost:6379

Run the test case. If you have followed me till now, the test case will fail with the below error

java.lang.IllegalStateException: Failed to load ApplicationContext...Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisConnectionFactory' defined in class path resource 
...

Add the below dependencies in build.gradle

implementation group: ‘org.apache.commons’, name: ‘commons-pool2’, version: ‘2.6.2’implementation group: ‘redis.clients’, name: ‘jedis’, version: ‘2.9.3’

Once you add these, the test case will fail with the below error

java.lang.IllegalStateException: Failed to load ApplicationContextat org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:125)Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘redisTemplate’ defined in class path resource [com/sudeep/redis/introspringdataredis/Configuration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: RedisConnectionFactory is required...
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)

Redis is complaining that we are trying to create a RedisTemplate without a connection factory. In the Spring Data Redis documentation, for whatever reason, they decided to go the hybrid approach with respect to initializing beans through Annotation and XML and gave the Redis Connection Factory setup only in XML. In order to fix the error, set the connection factory on RedisTemplate as below

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}

Now, the test case will run successfully.

Create an API for accessing the Application

Next, we need an API Controller for accessing the application. Create a Controller class with a save method that saves a Person object and returns the generated id.

package com.sudeep.redis.introspringdataredis;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;


@RestController
public class IntroSpringDataRedisController {

private PersonRepository personRepository;

public IntroSpringDataRedisController(PersonRepository personRepository) {
this.personRepository = personRepository;
}
@PostMapping("/save")
public ResponseEntity<String> save(@RequestParam String firstName, @RequestParam String lastName) {
if(firstName == null) {
return ResponseEntity.badRequest().build();
}
Person person = Person.builder()
.firstname(firstName)
.lastname(lastName)
.address(Address.builder()
.street("street")
.city("city").build()
).build();
Person savedPerson = personRepository.save(person);
return ResponseEntity.ok(savedPerson.getId());
}
}

Run the controller tests and they should pass but running the ApplicationTests will give you an error as below

java.lang.IllegalStateException: Failed to load ApplicationContextCaused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘introSpringDataRedisController’ defined in file [/Users/sudeepmoothedath/IdeaProjects/intro-spring-data-redis/out/production/classes/com/sudeep/redis/introspringdataredis/IntroSpringDataRedisController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.sudeep.redis.introspringdataredis.PersonRepository’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.sudeep.redis.introspringdataredis.PersonRepository’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

We are trying to use constructor injection in Controller to inject the PersonRepository. Spring was not able to find any beans of type PersonRepository.

In order to register the PersonRepository as a Redis Repository, add the below annotation to the Configuration class.

@EnableRedisRepositories

Even after this, the test case will still fail with the same error as above. Even though the error says that no Beans are registered, it doesn’t say why.

It is challenging to find the root cause and fix it, as the above message is misleading. I tried to run the app and got the same error, but the startup log had some more information that narrowed down the issue. The startup log has the below lines

INFO 13010 — — [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!INFO 13010 — — [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.INFO 13010 — — [ main] .RepositoryConfigurationExtensionSupport : Spring Data Redis — Could not safely identify store assignment for repository candidate interface com.sudeep.redis.introspringdataredis.PersonRepository.

This was really useful. It clearly states that there are multiple Spring Data modules found and as a result, Spring Data Redis couldn’t safely identify how to store Person class defined as a type in the PersonRepository. As a result, Spring did not initialize the PersonRepository bean which resulted in the error shown while running the test case.

I had followed almost every step in the Spring Data Redis documentation, except one. The code snippet in the documentation has the annotation @RedisHash added to the Person class. I did not add this purposefully as I didn’t want the object to be stored as a Hash in Redis. Adding that annotation fixed the error but that increased my curiosity. On digging further, I was able to identify the reason for the error and what that annotation does.

Spring Data Redis dependency in Gradle brings in 2 Spring Data modules — Spring Data Redis and Spring Data Key Value. This is the reason for the multiple Data modules found error. So, as a result of this, Spring doesn’t know whether to save the object in Redis or KeyValue store. By adding @RedisHash on Person class, we are telling Spring to save it in Redis. For the purpose of this blog, i’m continuing with using Hash.

Once you add the annotation, the Person class should look like below.

...
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
@AllArgsConstructor
@Getter
@Builder
@RedisHash
public class Person {
private String id;
@Indexed private String firstname;
private String lastname;
private Address address;
}

Now if you run all the test cases in the app, everything should pass and you’ll be able to start the application without any errors.

Here, i’ve added @Indexed annotation for firstName. @Id is optional since the field is named id. @Indexed is needed to query by firstName. Without that, the query can only be executed using the key. You can also add a keyspace by specifying a name for the entity. For ex: @RedisHash("people"). This will help during querying; as otherwise, the key generated in Redis will include the fully qualified classname.

Running in local

In order to run this app in local, either the Redis server should be installed on the machine or if you have docker installed, use the docker-compose provided along with the code. Running docker-compose up -d will spin up a container with Redis running on port 6379. -d executes the process in background.

The code sample in Git has rest of the information including the implementation of some CRUD operations in the Controller. There is not much to add about it in this post.

Testing in local

Use curl or Postman to invoke the save with First name and Last name as input. For ex:

curl -X POST “http://localhost:8080/save?firstName=John&lastName=Doe"

Retrieve the data using the persons or personsByFirstName operations.

Summary

This article tries to provides detailed steps to implement Spring Data Redis with Repositories, including details about some of the annotations used in the example along potential issues you could face and how to resolve them. The final code can be found in Git.

--

--