学习

增强 Java 对象到哈希的映射

介绍#

Spring Data Redis (SDR) 框架使编写使用 Redis 作为 Java 对象 (POJO) 存储的 Spring 应用程序变得容易,因为它消除了通过 Spring 的出色基础设施支持与存储进行交互所需的冗余任务和样板代码。

Redis OM Spring 在 SDR 之上构建,通过利用 Redis 丰富的模块生态系统来改善和优化与 Redis 的交互。对于使用 SDR 的 @RedisHash 注释映射的 Java 对象,我们通过以下方式增强了对象映射:

  • 消除了对客户端维护的二级索引的需要,而是使用 Redis 本地的搜索和查询引擎。
  • 实现具有快速灵活查询的动态存储库查找器
  • 使用 ULID 而不是传统的 UUID 来提高性能、可读性和互操作性

您将构建的内容#

您将构建一个将 User POJO (普通旧 Java 对象) 存储为 Redis 哈希的应用程序。

您需要的内容#

从 Spring Initializr 开始#

我们将使用 Spring Initializr创建基本 SpringBoot 应用程序。您可以使用此 预初始化的项目 并单击生成以下载 ZIP 文件。该项目已配置为适合本教程中的示例。

配置项目

  • 导航到 https://start.spring.io. 此服务会引入您应用程序所需的所有依赖项,并为您完成大部分设置。
  • 选择 Gradle 或 Maven 以及您要使用的语言。本指南假设您选择了 Java。
  • 单击 并选择
  • 单击
  • 下载生成的 ZIP 文件 (roms-hashes.zip),它是一个配置了您选择的 Web 应用程序的存档。

包含的依赖项

  • : 使用 Spring MVC 构建 Web/RESTful 应用程序。它将使我们能够将应用程序公开为 Web 服务。
  • : Java 注释库,有助于减少样板代码。
  • : 提供快速应用程序重启、LiveReload 和配置,以增强开发体验。
注意

如果您的 IDE 集成了 Spring Initializr,您可以在 IDE 中完成此过程。

注意

您也可以从 Github 分叉项目,并在 IDE 或其他编辑器中打开它。

添加 Redis OM Spring#

Maven#

要使用 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#

如果使用 gradle,请按如下方式添加依赖项

dependencies {
  implementation 'com.redis.om.spring:redis-om-spring:0.1.0-SNAPSHOT'
}

启用 Redis 存储库#

生成的应用程序包含单个 Java 文件,即 @SpringBootApplication 注释的主应用程序。要启用 Spring Data Redis 存储库,我们还需要使用 @EnableRedisEnhancedRepositories 以及 @Configuration 注释来注释主类或配置类。

package com.redis.om;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.redis.om.spring.annotations.EnableRedisEnhancedRepositories;

@SpringBootApplication
@EnableRedisEnhancedRepositories(basePackages = "com.redis.om.hashes.*")
public class RomsHashesApplication {

    public static void main(String[] args) {
        SpringApplication.run(RomsHashesApplication.class, args);
    }

}

🚀 启动 Redis#

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

域实体#

我们的应用程序中将只有一个类,即 User 类。我们将使用 lombok 来避免创建 getter 和 setter。我们将使用 lombok 注释 @Data@RequiredArgsConstructor@AllArgsConstructor

最后,要将类标记为 JSON 文档,我们将使用 @Document 注释。

package com.redis.om.hashes.domain;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

import com.redis.om.spring.annotations.Bloom;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@RedisHash
public class User {
  @Id
  private String id;

  @Indexed @NonNull
  private String firstName;

  @Indexed
  private String middleName;

  @Indexed @NonNull
  private String lastName;

  @NonNull
  @Indexed
  String email;
}

我们使用 Spring Data Redis @RedisHash 注释。名为 id 的属性使用 org.springframework.data.annotation.Id 注释。这两个项目负责创建用于在 Redis 中持久保存哈希的实际密钥。

User 类包含一个 firstNamemiddleNamelastName,以及一个 email 属性。

package com.redis.om.hashes.repositories;

import java.util.List;
import java.util.Optional;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.redis.om.hashes.domain.User;

@Repository
public interface UserRepository extends CrudRepository<User, String> {
}

预填充数据库#

让我们通过修改 RomsHashesApplication 类来添加一些 User POJO 到应用程序启动时的 Redis 中,使用 @Autowired 注解包含新创建的 UserRepository。然后我们将使用一个 CommandLineRunner @Bean 注解的方法创建四个 User POJO 并将它们保存到数据库中。

CommandLineRunner 中,我们执行以下步骤:

  • 使用存储库的 deleteAll 方法清除数据库(在生产环境中要小心!🙀)
  • 创建四个 User 实例;我们将使用 Rage Against the Machine 的四名乐队成员。
  • 使用存储库的 saveAll 方法批量保存所有 User POJO。
package com.redis.om.hashes;

import java.util.List;

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 com.redis.om.hashes.domain.User;
import com.redis.om.hashes.repositories.UserRepository;
import com.redis.om.spring.annotations.EnableRedisEnhancedRepositories;

@SpringBootApplication
@Configuration
@EnableRedisEnhancedRepositories(basePackages = "com.redis.om.hashes.*")
public class RomsHashesApplication {

  @Autowired
  private UserRepository userRepo;

  @Bean
  CommandLineRunner loadTestData() {
    return args -> {
      userRepo.deleteAll();

      User john = User.of("Zack", "de la Rocha", "zack@ratm.com");
      User tim = User.of("Tim", "Commerford", "tim@ratm.com");
      User tom = User.of("Tom", "Morello", "tom@ratm.com");
      User brad = User.of("Brad", "Wilk", "brad@ratm.com");

      userRepo.saveAll(List.of(john, tim, tom, brad));
    };
  }

    public static void main(String[] args) {
        SpringApplication.run(RomsHashesApplication.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 检查它,以便了解系统的内部工作原理。

Redis 搜索索引#

在顶部你应该会看到 FT.CREATE 命令,它使用我们 POJO 中的注解来确定索引配方。由于我们的 POJO 用 @Document 注解,我们得到了一个针对以 com.redis.om.documents.domain.Company: 开头的任何键的 ON JSON 索引(这是 Spring Data Redis 和 ROMS 的默认键前缀)。

1638336613.156351 [0 172.19.0.1:63396] "FT.CREATE" "UserIdx" "ON" "HASH" "PREFIX" "1" "com.redis.om.hashes.domain.User:" "SCHEMA" "firstName" "AS" "firstName" "TAG" "middleName" "AS" "middleName" "TAG" "lastName" "AS" "lastName" "TAG" "email" "AS" "email" "TAG"

ROMS 使用用 @Indexed@Searchable 注解的 POJO 字段来构建索引模式。在 User POJO 的情况下,我们有 firstNamemiddleNamelastNameemail 字段都被注解为“可索引的”,这意味着我们可以对这些字段进行精确搜索。

Spring Data Redis 创建一个 SET 来维护我们实体的主键,ROMS 从 SDR 继承了此功能。在索引创建后的 DEL 命令是由我们数据加载方法中对 userRepo.deleteAll(); 的调用触发的。如果我们已经保存了任何对象,我们也会看到删除这些单独实例的调用。

对于每个 User POJO,我们应该会看到一系列 REDIS 命令,如:

1638340447.180533 [0 172.19.0.1:63398] "SISMEMBER" "com.redis.om.hashes.domain.User" "01FNTB6JWTQHMK7NTEYA8725MP"
1638340447.198702 [0 172.19.0.1:63398] "DEL" "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
1638340447.200874 [0 172.19.0.1:63398] "HMSET" "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP" "_class" "com.redis.om.hashes.domain.User" "email" "zack@ratm.com" "firstName" "Zack" "id" "01FNTB6JWTQHMK7NTEYA8725MP" "lastName" "de la Rocha"
1638340447.203121 [0 172.19.0.1:63398] "SADD" "com.redis.om.hashes.domain.User" "01FNTB6JWTQHMK7NTEYA8725MP"

首先,SDR 使用 SISMEMBER 命令检查对象是否已存在于 Redis SET 的主键中。然后,会发出 DEL 命令来删除 Hash,接着使用 HMSET 命令写入新的或更新的 Hash。最后,使用 SADD 命令将对象的 id 属性添加到主键集中。

让我们使用 Redis CLI 检查数据。我们将从列出以 com.redis.om.hashes.domain.User 为前缀的键开始。

127.0.0.1:6379> SCAN 0 MATCH com.redis.om.hashes.domain.User*
1) "0"
2) 1) "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
   2) "com.redis.om.hashes.domain.User:01FNTB6JZ2NSQNST3BBH1J1039"
   3) "com.redis.om.hashes.domain.User:01FNTB6JYP4X15EAF08YBK55WR"
   4) "com.redis.om.hashes.domain.User:01FNTB6JYXAZ6H7AJ9ZWHHW73H"
   5) "com.redis.om.hashes.domain.User"

我们有 5 个匹配项,每个创建的 User POJO 都有一个,再加上用于主键的 Redis SET。让我们检查一些值。

让我们检查存储在 com.redis.om.documents.domain.Company 键中的数据类型。

127.0.0.1:6379> TYPE "com.redis.om.hashes.domain.User"
set

知道了它是一个 Redis SET,让我们使用 SMEMBERS 命令检查它的内容。

127.0.0.1:6379> SMEMBERS "com.redis.om.hashes.domain.User"
1) "01FNTB6JZ2NSQNST3BBH1J1039"
2) "01FNTB6JWTQHMK7NTEYA8725MP"
3) "01FNTB6JYXAZ6H7AJ9ZWHHW73H"
4) "01FNTB6JYP4X15EAF08YBK55WR"

该集合包含我们所有用户的 Id。现在,让我们调查 com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ 键。

127.0.0.1:6379> TYPE "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
hash

存储的 Redis 数据类型是一个 hash(Redis Hash)。让我们使用 HGETALL 命令检查它的内容。

127.0.0.1:6379> HGETALL "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
 1) "_class"
 2) "com.redis.om.hashes.domain.User"
 3) "email"
 4) "zack@ratm.com"
 5) "firstName"
 6) "Zack"
 7) "id"
 8) "01FNTB6JWTQHMK7NTEYA8725MP"
 9) "lastName"
10) "de la Rocha"

查询#

ROMS 最引人注目的功能是在运行时从存储库接口自动创建存储库实现。

让我们从 UserRepository 中的简单方法声明开始,该方法将根据用户姓氏查找唯一实例的 User

package com.redis.om.hashes.repositories;

import java.util.Optional;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.redis.om.hashes.domain.User;

@Repository
public interface UserRepository extends CrudRepository<User, String> {
  Optional<User> findOneByLastName(String lastName);
}

ROMS 使用方法名称、参数和返回类型来确定要生成的正确查询,以及如何打包和返回结果。

findOneByLastName 返回一个 OptionalUser,这告诉 ROMS 如果未找到实体,则返回空有效负载。findOne 部分也强调即使有多个结果,我们也只对获取一个结果感兴趣。ROMS 解析方法名称以确定预期参数的数量,方法的 ByLastName 部分告诉我们我们期望一个名为 lastName 的参数。

测试控制器#

让我们创建一个 REST 控制器来测试 findOneByLastName 方法。在 com.redis.om.hashes.controllers 包下创建 UserController,如所示。

package com.redis.om.hashes.controllers;

import java.util.Optional;

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.hashes.domain.User;
import com.redis.om.hashes.repositories.UserRepository;

@RestController
@RequestMapping("/api/users")
public class UserController {

  @Autowired
  private UserRepository userRepository;

  @GetMapping("name/{lastName}")
  Optional<User> byName(@PathVariable("lastName") String lastName) {
    return userRepository.findOneByLastName(lastName);
  }

}

在我们的控制器中,我们包含了我们的 UserRepository,并创建了一个简单的函数来响应 /api/users/name/{lastName} 上的 GET 请求,其中 {lastName} 将是我们作为 lastName 传递的字符串参数,用于查找。

让我们使用 CURL 测试端点,并传递精确的公司名称 Redis

curl --location --request GET 'https://localhost:8080/api/users/name/Morello'
{"id":"01FNTB6JYXAZ6H7AJ9ZWHHW73H","firstName":"Tom","middleName":null,"lastName":"Morello","email":"tom@ratm.com"}

让我们格式化结果 JSON。

{
  "id": "01FNTB6JYXAZ6H7AJ9ZWHHW73H",
  "firstName": "Tom",
  "middleName": null,
  "lastName": "Morello",
  "email": "tom@ratm.com"
}

检查 Redis CLI MONITOR,我们应该会看到发出的查询。

1638342334.137327 [0 172.19.0.1:63402] "FT.SEARCH" "UserIdx" "@lastName:{Morello} "

假设我们要根据 lastNamefirstName 的组合查找 Users,我们可以向存储库接口添加查询声明,如下所示。

List<User> findByFirstNameAndLastName(String firstName, String lastName);

在这种情况下,方法 findByFirstNameAndLastName 被解析,并且使用 And 关键字来确定该方法期望两个参数;firstNamelastName

为了测试它,我们可以向我们的控制器添加以下内容。

@GetMapping("/q")
public List<User> findByName(@RequestParam String firstName, @RequestParam String lastName) {
  return userRepository.findByFirstNameAndLastName(firstName, lastName);
}

使用 CURL 测试我们

curl --location --request GET 'https://localhost:8080/api/users/q?firstName=Brad&lastName=Wilk'
[{"id":"01FNTE5KWCZ5H438JGB4AZWE85","firstName":"Brad","middleName":null,"lastName":"Wilk","email":"brad@ratm.com"}]

格式化结果 JSON,我们可以看到 Brad Wilk 的记录作为 JSON 数组结果的唯一元素被返回。

[
  {
    "id": "01FNTE5KWCZ5H438JGB4AZWE85",
    "firstName": "Brad",
    "middleName": null,
    "lastName": "Wilk",
    "email": "brad@ratm.com"
  }
]

回到 Redis CLI 监视器,我们可以看到由我们存储库方法生成的查询。

1638343589.454213 [0 172.19.0.1:63406] "FT.SEARCH" "UserIdx" "@firstName:{Brad} @lastName:{Wilk} "

Redis OM Spring 扩展了 Spring Data Redis,通过使用 Redis 本地的搜索和查询引擎,它提供了与 JPA 查询相媲美的搜索功能。