如何使用Redis作為關(guān)系數(shù)據(jù)庫
Redis可以用作關(guān)系數(shù)據(jù)庫嗎?
相信大多數(shù)人在使用Redis時都把它作為服務的緩存。而在Java + Spring中使用Redis有卻可以實現(xiàn)關(guān)系數(shù)據(jù)庫的功能。
要在Redis中實現(xiàn)這一點,首先,我們需要在Redis中安裝插件,包括:RedisJSON和RediSearch。其中RedisJSON允許我們以JSON格式存儲對象,RediSearch允許我們通過對象的任何字段進行搜索,甚至是嵌套字段。
為了在java端使用Redis,可以選擇Spring Data JPA,使用優(yōu)秀的Redis OM Spring庫(https://github.com/redis/redis-om-spring),它允許我們使用數(shù)據(jù)庫的抽象。Redis OM Spring擁有Spring和Jedis使用數(shù)據(jù)庫所需的所有依賴。
下面是一個Spring應用程序小案例,分別使用Mysql和Redis作為數(shù)據(jù)庫。在數(shù)據(jù)庫中存儲的是一個包含內(nèi)部聯(lián)接的關(guān)系數(shù)據(jù)庫對象。
實現(xiàn)案例
我們假設(shè)需要將一個名為"downtime"(停機記錄)的實體寫入數(shù)據(jù)庫。在這個實體中,添加了其他對象,如"place"(位置)、"reason"(原因)等。
關(guān)系數(shù)據(jù)庫的實體:
Mysql ,Java對象:
@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;
...
Redis,Java對象:
@Document
public class DowntimeDoc {
@Id
@Indexed
private String id;
@Indexed
private LocalDateTime beginDate;
private LocalDateTime endDate;
@Indexed
private PlaceDoc area;
@Indexed
private CauseDoc cause;
....
在上面的例子中,我們使用@Entity代替@Document。這個注解表示對象是一個實體。它將存儲在關(guān)鍵字為 “包路徑+類名+ Idx”的數(shù)據(jù)庫中。
添加@Indexed注解的字段將被建立索引以供搜索。如果不指定此注釋,則這個字段將只是保存在數(shù)據(jù)庫中,而搜索該字段將返回空結(jié)果。所以,我們可以根據(jù)需要添加此注釋。數(shù)據(jù)庫中已存在的數(shù)據(jù)將被異步執(zhí)行建立索引。
接下來,創(chuàng)建一個java庫(repository),用于從數(shù)據(jù)庫中獲取數(shù)據(jù)。
Mysql,Java示例:
public interface DowntimeRepository extends JpaRepository<Downtime, String> {
}
Redis,Java示例:
public interface DowntimeRedisRepository extends RedisDocumentRepository<DowntimeDoc, String> {
}
以上Redis示例的不同之處在于它從RedisDocumentRepository接口擴展,這實現(xiàn)了Spring的標準CRUD接口。
Let's add a method to find the first downtime for the reason we specified.
接下來添加一個方法,通過cause(原因)查找第一個downtime。
Mysql,Java示例:
public interface DowntimeRepository extends JpaRepository<Downtime, String> {
Downtime findFirstByCauseIdOrderByBeginDate(String causeId);
}
Redis,Java示例:
public interface DowntimeRedisRepository extends RedisDocumentRepository<DowntimeDoc, String> {
DowntimeDoc findTopByCause_IdOrderByBeginDateAsc(String causeId);
}
我們注意到,如果通過抽象來編寫數(shù)據(jù)庫操作的Java代碼,幾乎沒有什么差異。此外,Redis OM Spring允許我們使用@Query注解自定義查詢語句,實現(xiàn)起來就像Spring Data JPA中的那樣。
下面是一個HQL查詢的示例:
Mysql,Java示例:
@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);
Redis,Java示例:
@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);
在Redis的例子中,我們只是簡單地指定了“WHERE”部分的條件。無需指出需要附加哪些字段,這是因為它總是從指定的Redis庫中提取數(shù)據(jù)。但是,我們不能調(diào)出所有字段,而是通過“returnFields”參數(shù)指定我們需要哪些字段,還可以指定排序、Limit和Offset。在HQL中并沒有這些功能的。在這個例子中,還傳遞了一個Pageable參數(shù),這個參數(shù)將在數(shù)據(jù)庫級別執(zhí)行,使得數(shù)據(jù)庫不用將所有數(shù)據(jù)拉入應用程序。這一點和Hibernate不同,hibernate實在應用程序中進行修剪。
另外,Redis OM Spring 類似于Stream API,允許我們使用EntityStream編寫查詢。
下面是使用EntityStream查詢的示例。
…
entityStream
.of(DowntimeDoc.class)
.filter(DowntimeDoc$.AREA_ID.in(filter.getWorkPlace().toArray(String[]::new)))
.filter(between + " | " + causes)
.map(mapper::toEntity)
.collect(Collectors.toList());
在這個例子中,我使用了元模型的過濾器,將字符串參數(shù)傳遞給第二個過濾器,以表示兩個過濾選項都有效。EntityStream接受一組中間操作,并在調(diào)用時執(zhí)行這一組操作。
使用Redis OM Spring的一些注意事項
無法使用UUID作為主鍵??梢詫UID作為字符串賦值給指定字段,并將其加入索引。但是在搜索時,你需要轉(zhuǎn)義空格
{2e5af82m\-02af\-553b\-7961\-168878aa521е}
另外,如果在RedisDocumentRepository中進行搜索,不會起作用,因為代碼中有這樣一個表達式:
String regex = "(\\$" + key + ")(\\W+|\\*|\\+)(.*)";
因此,為了通過這些字段進行搜索,您必須直接在RediSearch中編寫查詢。
在通過RedisDocumentRepository方法搜索時,如果希望返回一個集合,那么必須傳遞一個Pageable,指定行的大小或在@Query中指定大小;否則,最多返回10條記錄。
方法FT.SEARCH (@Query)只支持一個排序。這可以通過編寫FT.AGGREGATE (@Aggregation)查詢來解決。
總之
以上是使用Redis存儲、搜索一個帶有嵌套對象的。通過Repository抽象來處理數(shù)據(jù),和Spring Data JPA沒有太大區(qū)別,特別是使用一些簡單的函數(shù),如:save、delete、findAllBy,以及通過方法名稱進行查詢等等。