JSON格式已成为一种普遍的数据交换格式和存储格式,许多传统的数据库现在也支持JSON作为原生格式,并且一些文档型数据库(如CouchDB和MongoDB)越来越受欢迎。JSON作为一种数据格式消除了关系数据库模式的僵化,允许应用程序更自然地发展。
但您是否知道Redis是一个支持原生JSON的完整文档数据库?Redis Stack将JSON添加为原生Redis数据类型ReJSON-RL
,并且与Redis的搜索和查询引擎无缝集成。在本教程中,我们将使用Redis OM Spring构建一个简单的文档应用程序。
您将构建一个应用程序,该应用程序将Company
POJO(普通旧Java对象)作为JSON文档存储在Redis中。
我们将从使用 Spring Initializr 创建基本的SpringBoot应用程序开始。您可以使用这个 预初始化项目 并点击生成以下载ZIP文件。此项目已配置为适合本教程中的示例。
配置项目
roms-documents.zip
),这是一个包含您选择的Web应用程序的存档。包含的依赖项是
如果您的IDE具有Spring Initializr集成,则可以从您的IDE完成此过程。
您也可以从Github分叉项目,并在您的IDE或其他编辑器中打开它。
要使用Redis OM Spring,请打开pom.xml
文件,并将Redis OM Spring Maven依赖项添加到pom.xml文件的dependencies
元素中
<dependency>
<groupId>com.redis.om</groupId>
<artifactId>redis-om-spring</artifactId>
<version>0.5.2-SNAPSHOT</version>
</dependency>
请查看官方的 Redis OM Spring GitHub存储库 以获取最新版本信息
如果使用gradle,请按如下方式添加依赖项
dependencies {
implementation 'com.redis.om.spring:redis-om-spring:0.1.0-SNAPSHOT'
}
生成的应用程序包含一个文件,即@SpringBootApplications
带注解的主应用程序
package com.redis.om;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RomsDocumentsApplication {
public static void main(String[] args) {
SpringApplication.run(RomsDocumentsApplication.class, args);
}
}
要启用Redis文档存储库,我们添加@EnableRedisDocumentRepositories
,这将允许我们使用RedisDocumentRepository
类作为我们数据存储库的类型。
package com.redis.om;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
@SpringBootApplication
@EnableRedisDocumentRepositories(basePackages = "com.redis.om.documents.*")
public class RomsDocumentsApplication {
public static void main(String[] args) {
SpringApplication.run(RomsDocumentsApplication.class, args);
}
}
Redis OM Spring依赖于Redis Stack的功能。下面的docker compose YAML文件可以快速入门。您可以将其放置在项目的根文件夹中,并将其命名为docker-compose.yml
version: '3.9'
services:
redis:
image: 'redis/redis-stack:latest'
ports:
- '6379:6379'
volumes:
- ./data:/data
environment:
- REDIS_ARGS: --save 20 1
deploy:
replicas: 1
restart_policy:
condition: on-failure
要启动docker compose应用程序,请在命令行(或通过Docker Desktop)上克隆此存储库,并运行(从根文件夹)
docker compose up
我们还将启动一个Redis CLI实例,以便我们可以监视ROMS正在做什么。为此,我们将以监视模式启动Redis
redis-cli MONITOR
我们的应用程序中将只有一个类,即Company
类。我们将使用lombok来避免创建getter和setter。我们将使用lombok注解@Data
、@RequiredArgsConstructor
和@AllArgsConstructor
。
最后,要将该类标记为JSON文档,我们使用@Document
注解。
package com.redis.om.documents.domain;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.core.index.Indexed;
import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Searchable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Document
public class Company {
@Id
private String id;
@NonNull
@Searchable
private String name;
@Indexed
private Set<String> tags = new HashSet<String>();
@NonNull
private String url;
@NonNull
@Indexed
private Point location;
@NonNull
@Indexed
private Integer numberOfEmployees;
@NonNull
@Indexed
private Integer yearFounded;
private boolean publiclyListed;
}
请注意,它在自己的类型上有一个@Document
注解,并且一个名为id
的属性,该属性用org.springframework.data.annotation.Id
注解。这两个项目负责创建用于在Redis中持久化JSON文档的实际键。
我们公司的 POJO 由一个名为 name
和 url
的 String
属性组成,一个代表公司标签集合的 Set
,一个表示公司总部地理位置的 org.springframework.data.geo.Point
,两个代表公司员工数量和成立年份的 Integer
,以及一个表示公司是否上市的 boolean
。
使用 Redis OM Spring 文档存储库,您可以将领域对象无缝地转换为 Redis JSON 键并将其存储,应用自定义映射策略,以及使用 Redis 维护的二级索引。
要创建负责存储和检索的组件,我们需要定义一个存储库接口。 RedisDocumentRepository
扩展了核心 org.springframework.data.repository
包中熟悉的 PagingAndSortingRepository
。
让我们在 src/main/java/com/redis/om/documents/repositories
下创建一个基本的存储库,内容如下所示
package com.redis.om.documents.repositories;
import com.redis.om.documents.domain.Company;
import com.redis.om.spring.repository.RedisDocumentRepository;
public interface CompanyRepository extends RedisDocumentRepository<Company, String> {
}
空存储库声明是我们获得 POJO 的基本 CRUD 功能/分页和排序所需的一切。
CompanyRepository
扩展了 RedisDocumentRepository
接口。它使用的实体和 ID 类型,Company
和 String
,在 RedisDocumentRepository
上的泛型参数中指定。通过扩展 PagingAndSortingRepository
(它本身扩展了 CrudRepository
),我们的 CompanyRepository
继承了几个用于处理 Company 持久化的方法,包括用于保存、删除和查找 Company 实体的方法。
让我们向 Redis 添加几个 Company
POJO,以便我们可以拥有一些数据来玩,同时也了解 ROMS 如何将 POJO 序列化为 JSON。
修改 RomsDocumentsApplication
类,使用 @Autowired
注释包含新创建的 CompanyRepository
。然后,我们将使用 CommandLineRunner
@Bean
注释的方法来创建两个 Company
POJO 并将它们保存到数据库中。
在 CommandLineRunner
中,我们执行以下步骤
deleteAll
方法清除数据库(在生产环境中要小心!🙀)Company
实例;一个用于 Redis,另一个用于 Microsoft。包括名称、URL、地理位置、员工数量、成立年份,以及一组标签。save
方法,传递每个创建的 POJO。package com.redis.om.documents;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.geo.Point;
import com.redis.om.documents.domain.Company;
import com.redis.om.documents.repositories.CompanyRepository;
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
@SpringBootApplication
@Configuration
@EnableRedisDocumentRepositories(basePackages = "com.redis.om.documents.*")
public class RomsDocumentsApplication {
@Autowired
CompanyRepository companyRepo;
@Bean
CommandLineRunner loadTestData() {
return args -> {
companyRepo.deleteAll();
Company redis = Company.of("Redis", "https://redis.com", new Point(-122.066540, 37.377690), 526, 2011);
redis.setTags(Set.of("fast", "scalable", "reliable"));
Company microsoft = Company.of("Microsoft", "https://microsoft.com", new Point(-122.124500, 47.640160), 182268, 1975);
microsoft.setTags(Set.of("innovative", "reliable"));
companyRepo.save(redis);
companyRepo.save(microsoft);
};
}
public static void main(String[] args) {
SpringApplication.run(RomsDocumentsApplication.class, args);
}
}
由于我们正在使用 Spring Boot DevTools,如果您已经运行了应用程序,它应该已经重新启动/重新加载。如果没有,请使用 mvn
命令启动应用程序
./mvnw spring-boot:run
如果一切顺利,您应该看到熟悉的 Spring Boot 横幅出现
[INFO] --- spring-boot-maven-plugin:2.6.0-M1:run (default-cli) @ roms-documents ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.0-M1)
2021-11-30 09:45:58.094 INFO 36380 --- [ restartedMain] c.r.o.d.RomsDocumentsApplication : Starting RomsDocumentsApplication using Java 11.0.9 on BSB.lan with PID 36380 (/Users/bsb/Code/module-clients/java/high-level/redis-om/redis-om-spring/demos/roms-documents/target/classes started by briansam-bodden in /Users/bsb/Code/module-clients/java/high-level/redis-om/redis-om-spring/demos/roms-documents)
如果您正在观看 Redis CLI 监控器,您应该看到大量的输出飞过。让我们分解它并使用另一个 Redis CLI 检查它,以了解系统的内部工作原理。
在顶部,您应该看到 FT.CREATE
命令,该命令使用我们 POJO 中的注释确定索引配方。由于我们的 POJO 使用 @Document
注释,因此我们得到一个针对以 com.redis.om.documents.domain.Company:
开头的任何键的索引 ON JSON
(这是 Spring Data Redis 和 ROMS 的默认键前缀)
1638291270.881079 [0 172.19.0.1:63378] "FT.CREATE" "CompanyIdx" "ON" "JSON" "PREFIX" "1" "com.redis.om.documents.domain.Company:" "SCHEMA" "$.name" "AS" "name" "TEXT" "$.tags[*]" "AS" "tags" "TAG" "$.location" "AS" "location" "GEO" "$.numberOfEmployees" "AS" "numberOfEmployees" "NUMERIC" "$.yearFounded" "AS" "yearFounded" "NUMERIC"
ROMS 使用使用 @Indexed
或 @Searchable
注释的 POJO 字段来构建索引模式。在 Company
POJO 的情况下,我们有一个名为 "searchable" 的 name
属性,这意味着我们获得了对该字段的全文搜索功能。这反映在模式字段定义中 $.name AS name TEXT
。
另一方面,tags
字段使用 "indexable" 注释,这意味着我们得到一个类型为 TAG 的索引字段,这意味着我们可以按字段的精确值搜索公司。这再次反映在模式字段定义中:$.tags[*] AS tags TAG
Spring Data Redis 创建一个 SET 来维护我们实体的主键,ROMS 从 SDR 继承了此功能。索引创建后的 DEL
命令是由于我们在数据加载方法中调用了 companyRepo.deleteAll();
触发的。如果我们之前已经保存了任何对象,我们还会看到删除这些单个实例的调用。
1638291270.936493 [0 172.19.0.1:63378] "DEL" "com.redis.om.documents.domain.Company"
最后,对于每个 Company
POJO,我们应该看到一系列 REDIS 命令,例如
1638291270.958384 [0 172.19.0.1:63378] "SISMEMBER" "com.redis.om.documents.domain.Company" "01FNRW9V98CYQMV2YAB7M4KFGQ"
1638291270.966868 [0 172.19.0.1:63378] "JSON.SET" "com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ" "." "{\"id\":\"01FNRW9V98CYQMV2YAB7M4KFGQ\",\"name\":\"Redis\",\"tags\":[\"reliable\",\"fast\",\"scalable\"],\"url\":\"https://redis.com\",\"location\":\"-122.06654,37.37769\",\"numberOfEmployees\":526,\"yearFounded\":2011,\"publiclyListed\":false}"
1638291270.970030 [0 172.19.0.1:63378] "SADD" "com.redis.om.documents.domain.Company" "01FNRW9V98CYQMV2YAB7M4KFGQ"
第一行使用 SISMEMBER
命令检查对象是否已存在于 Redis SET 的主键中。然后,使用 JSON.SET
命令保存实体的 JSON 序列化。一旦该操作成功,对象的 id
属性将使用 SADD
命令添加到主键集中。
让我们使用 Redis CLI 检查数据。我们将从列出以 com.redis.om.documents.domain.Company
为前缀的键开始
127.0.0.1:6379> SCAN 0 MATCH com.redis.om.documents.domain.Company*
1) "0"
2) 1) "com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ"
2) "com.redis.om.documents.domain.Company:01FNRW9V9VFNG0MQCJDXZPEG3H"
3) "com.redis.om.documents.domain.Company"
我们有 3 个匹配项,一个用于每个创建的 Company
POJO,以及用于主键的 Redis SET。让我们检查其中一些值。
让我们检查在 com.redis.om.documents.domain.Company
键中存储的是什么类型的数据结构
127.0.0.1:6379> TYPE "com.redis.om.documents.domain.Company"
set
知道它是一个 Redis SET,让我们使用 SMEMBERS
命令检查它的内容
127.0.0.1:6379> SMEMBERS "com.redis.om.documents.domain.Company"
1) "01FNRW9V9VFNG0MQCJDXZPEG3H"
2) "01FNRW9V98CYQMV2YAB7M4KFGQ"
该集合包含我们所有公司的 ID。现在,让我们调查 com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ
键
127.0.0.1:6379> TYPE "com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ"
ReJSON-RL
存储的 Redis 数据类型是 ReJSON-RL
(Redis JSON 文档)。让我们使用 JSON.GET
命令检查它的内容
127.0.0.1:6379> JSON.GET "com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ"
"{\"id\":\"01FNRW9V98CYQMV2YAB7M4KFGQ\",\"name\":\"Redis\",\"tags\":[\"reliable\",\"fast\",\"scalable\"],\"url\":\"https://redis.com\",\"location\":\"-122.06654,37.37769\",\"numberOfEmployees\":526,\"yearFounded\":2011,\"publiclyListed\":false}"
在我们对 ROMS 如何序列化我们的 Company
POJO 有了新的了解后,让我们继续扩展我们 CompanyRepository
的功能,使其超越 CRUD。
ROMS 最引人注目的功能是能够在运行时从存储库接口自动创建存储库实现。
让我们从 CompanyRepository
中的简单方法声明开始,该方法将根据公司名称查找 Company
的唯一实例。
package com.redis.om.documents.repositories;
import java.util.Optional;
// ... other imports ommitted ...
public interface CompanyRepository extends RedisDocumentRepository<Company, String> {
// find one by property
Optional<Company> findOneByName(String name);
}
ROMS 使用方法名称、参数和返回类型来确定要生成的正确查询以及如何打包和返回结果。
findOneByName
返回 Company
的 Optional
,这告诉 ROMS 如果未找到实体,则返回空有效负载。 findOne
部分也强调,即使有多个结果,我们也只对获得一个结果感兴趣。ROMS 解析方法名称以确定预期参数的数量,方法的 ByName
部分告诉我们,我们期望一个名为 name
的单个参数。
让我们创建一个 REST 控制器来测试 findOneByName
方法。在 com.redis.om.documents.controllers
包下创建 CompanyController
,如下所示
package com.redis.om.documents.controllers;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.redis.om.documents.domain.Company;
import com.redis.om.documents.repositories.CompanyRepository;
@RestController
@RequestMapping("/api/companies")
public class CompanyController {
@Autowired
CompanyRepository repository;
@GetMapping("name/{name}")
Optional<Company> byName(@PathVariable("name") String name) {
return repository.findOneByName(name);
}
}
在我们的控制器中,我们包含了 CompanyRepository
并创建了一个简单的函数来响应在 /api/companies/name/{name}
的 GET 请求,其中 {name}
是我们传递作为 name
参数用于查找的字符串。
让我们使用 CURL 测试端点,并传递公司名称 Redis
。
➜ curl --location --request GET 'https://localhost:8080/api/companies/name/Redis'
{"id":"01FNRW9V98CYQMV2YAB7M4KFGQ","name":"Redis","tags":["reliable","fast","scalable"],"url":"https://redis.com","location":{"x":-122.06654,"y":37.37769},"numberOfEmployees":526,"yearFounded":2011,"publiclyListed":false}
让我们格式化生成的 JSON。
{
"id": "01FNRW9V98CYQMV2YAB7M4KFGQ",
"name": "Redis",
"tags": ["reliable", "fast", "scalable"],
"url": "https://redis.com",
"location": {
"x": -122.06654,
"y": 37.37769
},
"numberOfEmployees": 526,
"yearFounded": 2011,
"publiclyListed": false
}
检查 Redis CLI Monitor 监控器显示生成的查询结果。
1638344903.218982 [0 172.19.0.1:63410] "FT.SEARCH" "CompanyIdx" "@name:Redis "
请注意,您可以使用 redis
(全部小写)或 rEdI
,并且您将获得匹配 Redis
的结果。如果您尝试使用少于 4 个字符的字符串,例如 red
或 RED
,则您将不会获得匹配结果。Redis 将最小字符串匹配大小限制为 4 个字符,以防止返回潜在的数百万条结果。
ROMS 支持 GeoJSON 类型以存储地理空间数据。通过在我们的查询中使用 near
关键字,我们告诉 ROMS 预期使用 Point
(org.springframework.data.geo.Point
) 和 Distance
(org.springframework.data.geo.Distance
) 类型作为参数。
// geospatial query
Iterable<Company> findByLocationNear(Point point, Distance distance);
让我们在控制器中添加一个测试端点,用于我们的地理空间查询。
@GetMapping("near")
Iterable<Company> byLocationNear(//
@RequestParam("lat") double lat, //
@RequestParam("lon") double lon, //
@RequestParam("d") double distance) {
return repository.findByLocationNear(new Point(lon, lat), new Distance(distance, Metrics.MILES));
}
在我们的控制器函数中,我们获取两个请求参数:纬度 lat
,经度 lon
和距离 d
(以英里为单位)。我们使用这些值构建存储库 findByLocationNear
函数所需的 Point
和 Distance
。
让我们使用 CURL 测试该函数,使用靠近 Redis 山景城总部的位置。
➜ curl --location --request GET 'https://localhost:8080/api/companies/near?lat=37.384&lon=-122.064&d=30'
[{"id":"01FNRW9V98CYQMV2YAB7M4KFGQ","name":"Redis","tags":["reliable","fast","scalable"],"url":"https://redis.com","location":{"x":-122.06654,"y":37.37769},"numberOfEmployees":526,"yearFounded":2011,"publiclyListed":false}]
格式化 JSON 结果,我们得到一个包含一个条目 Redis
的 JSON 数组。
[
{
"id": "01FNRW9V98CYQMV2YAB7M4KFGQ",
"name": "Redis",
"tags": ["reliable", "fast", "scalable"],
"url": "https://redis.com",
"location": {
"x": -122.06654,
"y": 37.37769
},
"numberOfEmployees": 526,
"yearFounded": 2011,
"publiclyListed": false
}
]
检查 Redis CLI Monitor 监控器显示生成的查询结果。
1638344951.451871 [0 172.19.0.1:63410] "FT.SEARCH" "CompanyIdx" "@location:[-122.064 37.384 30.0 mi] "
在某些情况下,您可能只需要使用 Redis Stack 的原始查询功能(就像在您需要使用原始 SQL 而不是 JPA 时一样)。对于这些场景,我们提供了 @Query
(com.redis.om.spring.annotations.Query
) 和 @Aggregation
(com.redis.om.spring.annotations.Aggregation
) 注解。这些注解公开了 JRediSearch
库提供的原始查询 API。ROMS 添加了参数解析和结果映射,因此您可以在存储库中使用原始查询和聚合。
// find by tag field, using JRediSearch "native" annotation
@Query("@tags:{$tags}")
Iterable<Company> findByTags(@Param("tags") Set<String> tags);
让我们使用 CURL 测试它。
➜ curl --location --request GET 'https://localhost:8080/api/companies/tags?tags=reliable'
[{"id":"01FNTF7QKAGCQYMWWBV3044DHW","name":"Redis","tags":["reliable","fast","scalable"],"url":"https://redis.com","location":{"x":-122.06654,"y":37.37769},"numberOfEmployees":526,"yearFounded":2011,"publiclyListed":false},{"id":"01FNTF7QKXJ1CNZERHADN91YBR","name":"Microsoft","tags":["reliable","innovative"],"url":"https://microsoft.com","location":{"x":-122.1245,"y":47.64016},"numberOfEmployees":182268,"yearFounded":1975,"publiclyListed":false}]
格式化 JSON,我们可以看到结果包含具有标签 reliable
的公司。
[
{
"id": "01FNTF7QKAGCQYMWWBV3044DHW",
"name": "Redis",
"tags": ["reliable", "fast", "scalable"],
"url": "https://redis.com",
"location": {
"x": -122.06654,
"y": 37.37769
},
"numberOfEmployees": 526,
"yearFounded": 2011,
"publiclyListed": false
},
{
"id": "01FNTF7QKXJ1CNZERHADN91YBR",
"name": "Microsoft",
"tags": ["reliable", "innovative"],
"url": "https://microsoft.com",
"location": {
"x": -122.1245,
"y": 47.64016
},
"numberOfEmployees": 182268,
"yearFounded": 1975,
"publiclyListed": false
}
]
检查 Redis CLI Monitor 监控器,我们可以看到生成结果的查询。
1638345120.384300 [0 172.19.0.1:63412] "FT.SEARCH" "CompanyIdx" "@tags:{reliable} "
与其他基于 Spring Data 的库一样,ROMS 可以使用各种逻辑和数值运算符处理各种查询,例如 between
、startingWith
、greaterThan
、lessThanOrEquals
等等。
以下是一些其他可能的示例。
// find by numeric property
Iterable<Company> findByNumberOfEmployees(int noe);
// find by numeric property range
Iterable<Company> findByNumberOfEmployeesBetween(int noeGT, int noeLT);
// starting with/ending with
Iterable<Company> findByNameStartingWith(String prefix);
这只是对 Redis OM Spring (ROMS) 功能的简要概述。在下一部分中,我们将介绍 ROMS 如何扩展 Spring Data Redis Redis 哈希映射,使其更加强大。