Can Redis Be Used as a Relational Database?
Explore how easy it is to work with Redis in Java + Spring, and find five differences from a relational database.
Join the DZone community and get the full member experience.
Join For FreeLet's start with the question, "How do you use Redis?" I'm sure most use it as a cache for the service. I hope you know that it can do more than just that. Recently, I spoke at a conference with a report on how we moved part of the data to Redis and requests fly to it in the first place. Now I want to tell you not about how we applied it, but about the fact that when working with Spring and its abstractions, you may not immediately notice the substitution.
Let's try to write a small Spring app that will use two PostgreSQL and Redis databases. I want to note that we will store in the databases not some kind of flat object, but a full-fledged object from a relational database with nested fields (inner join). To do this, we need plugins that need to be installed in Redis such as RedisJSON and RediSearch. The first allows us to store our object in JSON format, and the second allows us to search by any field of our object, even nested fields.
To work with a relational database, we will choose Spring Data JPA. And to work with Redis, we will use the excellent Redis OM Spring library, which allows you to work with the database at the abstraction level. This is an analog of Data JPA. Under the hood, Redis OM Spring has all the necessary dependencies for Spring and Jedis to work with the database. We will not dwell on the details, since the article is not about that.
Let's Write Code
So let's write code. Let's say we need to write a certain entity called "downtime"
to the database. In this entity, I added other objects such as "place"
, "reason"
, and others.
Entity for a relational database:
@Entity
@Table(schema = "test", name = "downtime")
public class Downtime {
@Id
private String id;
private LocalDateTime beginDate;
private LocalDateTime endDate;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "area")
private Place area;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "cause")
private Cause cause;
...
This piece of code does not need comments. We need to do the same for Redis.
Object for Redis:
@Document
public class DowntimeDoc {
@Id
@Indexed
private String id;
@Indexed
private LocalDateTime beginDate;
private LocalDateTime endDate;
@Indexed
private PlaceDoc area;
@Indexed
private CauseDoc cause;
....
In this case, instead of @Entity
, we use @Document
. This annotation indicates that our object is an entity. It will be stored in the database under the key “package path + class name + Idx.”
The @Indexed
annotation means that it will be indexed for search. If you do not specify this annotation, then this field will be saved in the database, but searching for it will return an empty result. You can add this annotation as needed. Data that is already in the database will be indexed asynchronously; new data will be indexed synchronously.
Next, we will make a repository, which basically works to get data from the database.
An example for a relational database:
public interface DowntimeRepository extends JpaRepository<Downtime, String> {
}
Example for Redis:
public interface DowntimeRedisRepository extends RedisDocumentRepository<DowntimeDoc, String> {
}
The difference is that we extend the current interface from RedisDocumentRepository
, which extends the standard CRUD interface for Spring.
Let's add a method to find the first downtime for the reason we specified.
public interface DowntimeRepository extends JpaRepository<Downtime, String> {
Downtime findFirstByCauseIdOrderByBeginDate(String causeId);
}
And the same for Redis:
public interface DowntimeRedisRepository extends RedisDocumentRepository<DowntimeDoc, String> {
DowntimeDoc findTopByCause_IdOrderByBeginDateAsc(String causeId);
}
As you noticed, if you write code working with the database through abstractions, then the difference is almost not noticeable. In addition, Redis OM Spring allows you to write queries yourself using the @Query
annotation, as in Spring Data JPA.
Here is an example of an HQL query:
@Query("SELECT d FROM Downtime d" +
" JOIN FETCH d.area " +
" JOIN FETCH d.cause" +
" JOIN FETCH d.fixer" +
" JOIN FETCH d.area.level " +
" WHERE d.area IN ?1 AND (d.beginDate BETWEEN ?2 AND ?3 OR d.cause IN ?4) ")
List<Downtime> findAllByParams(List<Place> workPlace, LocalDateTime start, LocalDateTime end, List<Cause> causes);
Same for Redis:
@Query("(@area_id:{$areas} ) & (@beginDate:[$start $end] | @cause_id:{$causes})")
Page<DowntimeDoc> findByParams(@Param("areas") List<String> areas,
@Param("start") long start,
@Param("end") long end,
@Param("causes") List<String> causes, Pageable pageable);
In the case of Redis, we simply specify the conditions for the “WHERE”
section. It is not necessary to indicate which fields need to be attached since they are always pulled from the database. However, we can not pull up all the fields but specify with the additional “returnFields”
parameter what exactly we need. You can also specify sorting, limit, and offset - the latter, by the way, is impossible in HQL. In this example, I passed Pageable
to the method, and it will work at the database level, not pull all the data into the service, and trim it in it (as would be the case with Hibernate).
Also, Redis OM Spring allows you to write queries using EntityStream
, which is analogous to Stream API.
Here is an example of the above queries using EntityStream
.
…
entityStream
.of(DowntimeDoc.class)
.filter(DowntimeDoc$.AREA_ID.in(filter.getWorkPlace().toArray(String[]::new)))
.filter(between + " | " + causes)
.map(mapper::toEntity)
.collect(Collectors.toList());
In this example, I'm using one filter using the metamodel, passing the parameters as a string to the second filter to show that both options are valid. You guessed it: EntityStream
accepts a set of intermediate operations and executes this set when calling a terminal operation.
Nuances of Redis OM Spring
Let me tell you about some of the nuances of using Redis OM Spring:
- You will not be able to use a UUID as a primary key. You can specify it as a string and it will be indexed. But when searching, you will need to escape spaces
@id
:
{2e5af82m\-02af\-553b\-7961\-168878aa521е}
And one more thing: if you search through the RedisDocumentRepository
repository, nothing will work, because there is such an expression in the code that will remove all screens:
String regex = "(\\$" + key + ")(\\W+|\\*|\\+)(.*)";
Therefore, in order to search by such fields, you will have to write a query directly in RediSearch. I have an example of how to do this in the demo project.
- When searching through the
RedisDocumentRepository
methods, if you expect a collection, then you must pass aPageable
indicating the size of the expected rows or specifying the size in@Query
; otherwise, you will receive a maximum of 10 records. - The
FT.SEARCH (@Query)
method supports only one parameter for sorting. This is solved by writing a query throughFT.AGGREGATE (@Aggregation)
.
The above list is not exhaustive. While working with these libraries, I found many different things, but this is all just a specificity of the database implementation. Finally, I did not put information about Redis plugins in this article and did not talk about all the features of Redis OM Spring; otherwise, this article will be huge and not readable.
Conclusion
I showed that currently, Redis allows you to store an object with a large nesting and allows you to search through the fields of this object. If you are working with data through abstractions in the repository, then some may not see any difference from Spring Data JPA, especially if you use some simple queries like Save
, delete
, findAllBy
, etc., as well as queries through the name of the method.
Examples can be found on GitHub.
All success.
Opinions expressed by DZone contributors are their own.
Comments