Introduction to Spring Data Redis
Want to make Redis easier to use in your Spring app?
Join the DZone community and get the full member experience.
Join For FreeRedis is an in-memory data structure store with optional durability, used as a database, cache, and message broker. Currently, it is the most most popular tool in the key/value stores category: https://db-engines.com/en/ranking/key-value+store. The easiest way to integrate your application with Redis is through Spring Data Redis. You can use Spring RedisTemplate
directly for that, or you might as well use Spring Data Redis repositories. There are some limitations when you integrate with Redis via Spring Data Redis repositories. They require at least Redis Server version 2.8.0 and do not work with transactions. Therefore, you need to disable transaction support for RedisTemplate, which is leveraged by Redis repositories.
Redis is usually used for caching data stored in a relational database. In the current sample, it will be treated as a primary database — just for simplification.
Spring Data repositories do not require any deeper knowledge about Redis from a developer. You just need to annotate your domain class properly. As usual, we will examine main features of Spring Data Redis basing on the sample application. Supposing we have the system, which consists of three domain objects: Customer
, Account
, and Transaction
, here's the picture that illustrates relationships between elements of that system. Transaction
is always related with two accounts: sender ( fromAccountId
) and receiver ( toAccountId
). Each customer may have many accounts.
Although the picture visible above shows three independent domain models, customer and account is stored in the same, single structure. All customer's accounts are stored as a list inside customer object. Before proceeding to the sample application implementation details, let's begin by starting the Redis database.
1. Running Redis on Docker
We will run a Redis standalone instance locally using its Docker container. You can start it in in-memory mode or with the persistence store. Here's the command that runs a single, in-memory instance of Redis on a Docker container. It is exposed outside on default 6379
port.
$ docker run -d --name redis -p 6379:6379 redis
2. Enabling Redis Repositories and Configuring Connection
I'm using Docker Toolbox, so each container is available for me under the address 192.168.99.100
. Here's the only one property that I need to override inside configuration settings ( application.yml
).
spring:
application:
name: sample-spring-redis
redis:
host: 192.168.99.100
To enable Redis repositories for a Spring Boot application, we just need to include the single starter <code>spring-boot-starter-data-redis</code>.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
We may choose between two supported connectors: Lettuce and Jedis. For Jedis, I had to include one additional client's library to dependencies, so I decided to use simpler option — Lettuce, which does not require any additional libraries to work properly. To enable Spring Data Redis repositories, we also need to annotate the main or the configuration class with @EnableRedisRepositories
and declare theRedisTemplate
bean. Although we do not use RedisTemplate
directly, we still need to declare it while it is used by CRUD repositories for integration with Redis.
@Configuration
@EnableRedisRepositories
public class SampleSpringRedisConfiguration {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}
}
3. Implementing Domain Entities
Each domain entity, at least, has to be annotated with @RedisHash
and contain property annotated with @Id
. Those two items are responsible for creating the actual key used to persist the hash. Besides identifier properties annotated with @Id
, you may also use secondary indices. The good news about it is that it can be not only with dependent single objects but also on lists and maps. Here's the definition of Customer
entity. It is available on Redis under customer
key. It contains a list of Account
entities.
@RedisHash("customer")
public class Customer {
@Id private Long id;
@Indexed private String externalId;
private String name;
private List<Account> accounts = new ArrayList<>();
public Customer(Long id, String externalId, String name) {
this.id = id;
this.externalId = externalId;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
public void addAccount(Account account) {
this.accounts.add(account);
}
}
Account
does not have its own hash. It is contained by Customer
has as list of objects. The property id
is indexed on Redis in order to speed-up the search based on the property.
public class Account {
@Indexed private Long id;
private String number;
private int balance;
public Account(Long id, String number, int balance) {
this.id = id;
this.number = number;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
Finally, let's take a look at the Transaction
entity implementation. It uses only account ids, not the whole objects.
@RedisHash("transaction")
public class Transaction {
@Id
private Long id;
private int amount;
private Date date;
@Indexed
private Long fromAccountId;
@Indexed
private Long toAccountId;
public Transaction(Long id, int amount, Date date, Long fromAccountId, Long toAccountId) {
this.id = id;
this.amount = amount;
this.date = date;
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Long getFromAccountId() {
return fromAccountId;
}
public void setFromAccountId(Long fromAccountId) {
this.fromAccountId = fromAccountId;
}
public Long getToAccountId() {
return toAccountId;
}
public void setToAccountId(Long toAccountId) {
this.toAccountId = toAccountId;
}
}
4. Implementing Repositories
The implementation of repositories is the most pleasant part of our exercise. As usual, with Spring Data projects, the most common methods like save
, delete
, or findById
are already implemented. So, we only have to create our custom find methods if needed. Since the usage and implementation of thefindByExternalId
method is rather obvious, the method findByAccountsId
may not be. Let's move back to a model definition to clarify usage of that method. Transaction
contains only account ids; it does not have direct relationship with Customer
. What if we need to learn the details about customers besides just a given transaction? We can find a customer by one of its accounts from the list.
public interface CustomerRepository extends CrudRepository {
Customer findByExternalId(String externalId);
List findByAccountsId(Long id);
}
Here's the implementation of the repository for the Transaction
entity.
public interface TransactionRepository extends CrudRepository {
List findByFromAccountId(Long fromAccountId);
List findByToAccountId(Long toAccountId);
}
5. Building Repository Tests
We can easily test Redis repositories functionality using Spring Boot Test project with @DataRedisTest
. This test assumes you have a running instance of the Redis server on the already-configured address 192.168.99.100.
@RunWith(SpringRunner.class)
@DataRedisTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class RedisCustomerRepositoryTest {
@Autowired
CustomerRepository repository;
@Test
public void testAdd() {
Customer customer = new Customer(1L, "80010121098", "John Smith");
customer.addAccount(new Account(1L, "1234567890", 2000));
customer.addAccount(new Account(2L, "1234567891", 4000));
customer.addAccount(new Account(3L, "1234567892", 6000));
customer = repository.save(customer);
Assert.assertNotNull(customer);
}
@Test
public void testFindByAccounts() {
List<Customer> customers = repository.findByAccountsId(3L);
Assert.assertEquals(1, customers.size());
Customer customer = customers.get(0);
Assert.assertNotNull(customer);
Assert.assertEquals(1, customer.getId().longValue());
}
@Test
public void testFindByExternal() {
Customer customer = repository.findByExternalId("80010121098");
Assert.assertNotNull(customer);
Assert.assertEquals(1, customer.getId().longValue());
}
}
6. More Advanced Testing With Testcontainers
You may provide some advanced integration tests using Redis since your Docker container started during the test by the Testcontainer library. I have already published some articles about the Testcontainers framework. If you would like read more details about it, please refer to my previous articles: Microservices Integration Tests with Hoverfly and Testcontainers and Testing Spring Boot Integration with Vault and Postgres using Testcontainers Framework.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
public class CustomerIntegrationTests {
@Autowired
TestRestTemplate template;
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:5.0.3").withExposedPorts(6379);
@Before
public void init() {
int port = redis.getFirstMappedPort();
System.setProperty("spring.redis.host", String.valueOf(port));
}
@Test
public void testAddAndFind() {
Customer customer = new Customer(1L, "123456", "John Smith");
customer.addAccount(new Account(1L, "1234567890", 2000));
customer.addAccount(new Account(2L, "1234567891", 4000));
customer = template.postForObject("/customers", customer, Customer.class);
Assert.assertNotNull(customer);
customer = template.getForObject("/customers/{id}", Customer.class, 1L);
Assert.assertNotNull(customer);
Assert.assertEquals("123456", customer.getExternalId());
Assert.assertEquals(2, customer.getAccounts().size());
}
}
7. Viewing Data
Now, let's analyze the data stored in Redis after our JUnit tests. We may use one of GUI tool for that. I decided to install RDBTools available on site https://rdbtools.com. You can easily browse data stored on Redis using this tool. Here's the result for customer
entity with id=1
after running the JUnit test.
Here's the similar result for transaction
entity with id=1
.
Source Code
The sample application source code is available on GitHub in the repository sample-spring-redis.
Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments