学习

对象映射与 Redis 存储库

Brian Sam-Bodden
作者
Brian Sam-Bodden, Redis 开发者倡导者

目标#

配置 RedisTemplate 并了解如何使用不同的操作捆绑包从 Spring REST 控制器读取和写入 Redis 数据。

议程#

在本课中,您将学习

  • 如何使用 @RedisHash 注释映射域对象。
  • 如何使用 @Id 为映射的对象提供主键。
  • 如何使用 @Reference 创建对象之间的引用关联。
  • 如何创建和使用 Redis 存储库来存储域对象。

如果您遇到问题

用户和角色#

在许多应用程序中,无论是 Web 应用程序还是 RESTful Web 服务,第一个要实现的域部分之一是用户/角色子域。在 Redi2Read 中,用户拥有一个或多个角色,这是一种典型的 1 对多关联。

输入 @RedisHash#

在本节中,我们将开始创建 Redi2Read 域模型。我们将使用 Lombok 简化我们的 POJO,并使用 Spring Data Redis 的 @RedisHash 和其他 Spring Data 注释,我们将配置我们的模型以持久化到 Redis 中。

角色模型

让我们从创建最简单的域类开始,即位于目录 src/main/java/com/redislabs/edu/redi2read/models 下的 Role 类。让我们将文件命名为 Role.java,其内容如下

package com.redislabs.edu.redi2read.models;

import lombok.Data;

@Data
public class Role {
  private String id;
  private String name;
}

我们从一个使用 Lombok 的 @Data 注释进行注释的类开始,该注释添加了 @ToString@EqualsAndHashCode@Getter/@Setter 以及一个 @RequiredArgsContructor,从而为我们提供了一个完整的 Java POJO。为了让 Spring Data 存储库知道如何将 Role 的实例映射到 Redis Hash,我们需要使用 @RedisHash 注释对该类进行注释。

package com.redislabs.edu.redi2read.models;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import lombok.Builder;
import lombok.Data;

@Data
@RedisHash
public class Role {
  @Id
  private String id;

  private String name;
}

@RedisHash 可以将生存时间(以 Long 类型表示)和 String 值作为参数,这将覆盖默认的 Redis 键前缀(默认情况下,键前缀是类的完全限定名加上冒号)。

在类中,支持大多数常用的 Spring Data 注释。对于 Role,让我们使用 @Id 注释对 id 字段进行注释。Spring Data Redis 将为注释类型自动生成合适的 id。

用户模型

用户模型将用于注册/注册方法。为了允许执行服务器端验证,我们需要在 Maven POM 中添加对 spring-boot-starter-validation 库的依赖关系。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

现在我们可以创建 User 类,其内容如下:package com.redislabs.edu.redi2read.models;

import java.util.HashSet;
import java.util.Set;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

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

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
@Data
@RedisHash
public class User {
  @Id
  @ToString.Include
  private String id;

  @NotNull
  @Size(min = 2, max = 48)
  @ToString.Include
  private String name;

  @NotNull
  @Email
  @EqualsAndHashCode.Include
  @ToString.Include
  @Indexed
  private String email;

  @NotNull
  private String password;

  @Transient
  private String passwordConfirm;

  @Reference
  private Set<Role> roles = new HashSet<Role>();

  public void addRole(Role role) {
    roles.add(role);
  }
}

这个类稍微复杂一些,因此让我们将其分解

  1. 1.我们还有另一个 POJO (@Data),其实例可以持久化为 Redis Hash (@RedisHash)
  2. 2.该类被注释为仅将明确使用 @EqualsHashCode.Include@ToString.Include 进行注释的字段添加到 equals/hashcode/toString 中
  3. 3.再次,我们使用 @Id 生成一个自动生成的 String Redis Hash 键
  4. 4.通过使用 @Indexed 对属性进行注释,我们在 email 字段上创建了一个(次要)索引。我们将在下一课中详细了解次要索引。
  5. 5.几个 javax.validation.constraints 注释用于表示字段的类型为 @Email、为 @NotNull-able 以及限制其 @Size
  6. 6.passwordConfirm 字段将保存传统的“密码确认”,它被标记为 @Transient,因此 @RedisHash 不会尝试将其序列化到数据库中
  7. 7.对于 User 的角色,我们有一个被标记为 @ReferencesSet Role 对象,这将导致它们被存储为给定角色的 id,位于支持 User 实例的 Redis Hash 中。
  8. 8.最后,在底部,我们添加了一个实用程序方法来将 Role 添加到 User 的 Role Set 中。

Spring 存储库#

现在我们已经有了两个正确注释的模型,我们需要关联的存储库来对这些模型执行数据操作。Spring Data 存储库类似于 DAO(数据访问对象)模式,因为它们都抽象了针对底层数据存储的操作。不同之处在于,存储库通过专注于对象集合的管理进一步抽象了底层存储机制,而 DAO 则更侧重于 SQL/表。

角色存储库#

src/main/java/com/redislabs/edu/redi2read/repositories 目录下,我们创建一个名为 RoleRepository 的接口,如下所示:

package com.redislabs.edu.redi2read.repositories;

import com.redislabs.edu.redi2read.models.Role;

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

@Repository
public interface RoleRepository extends CrudRepository<Role, String> {
}

我们的存储库继承了 CrudRepository 用于 Role 类,其键类型为 String,它提供基本的 CRUD 和查找操作。Spring Data Redis 存储库具有很高的性能;它们避免使用反射和字节码生成,而是使用 Spring 的 ProxyFactory API 使用程序化的 JDK 代理实例。参见 https://bit.ly/2PshxEI

测试角色存储库#

让我们通过使用 CommandLineRunner 实现来测试 RoleRepository。Spring CommandLineRunner 是一个接口,它告诉 Spring 容器在启动时需要执行 run 方法。Spring Boot 应用程序可以拥有多个 CommandLineRunner;为了控制它们的执行顺序,我们可以进一步用 @Order 注解它们。创建目录 src/main/java/com/redislabs/edu/redi2read/boot,然后添加 CreateRoles

package com.redislabs.edu.redi2read.boot;

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class CreateRoles implements CommandLineRunner {

 @Override
 public void run(String... args) throws Exception {
   System.out.println(">>> Hello from the CreateRoles CommandLineRunner...");
 }
}

我们的 CreateRoles 类将在每次服务器启动或每次实时重新加载时运行(因为我们使用的是 Spring DevTools)。@Order 注解接受一个数字值,表示执行顺序。为了测试命令行运行器,我们在 run 方法中有一个 System.out.println ,我们可以在控制台上看到它。

2021-04-02 14:32:58.374  INFO 41500 --- [  restartedMain] c.r.edu.redi2read.Redi2readApplication   : Started Redi2readApplication in 0.474 seconds (JVM running for 74714.143)
>>> Hello from the CreateRoles CommandLineRunner...
2021-04-02 14:32:58.375  INFO 41500 --- [  restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged

既然我们知道 CreateRoles 组件运行,让我们完成它以与 RoleRepository 协作。

package com.redislabs.edu.redi2read.boot;

import com.redislabs.edu.redi2read.models.Role;
import com.redislabs.edu.redi2read.repositories.RoleRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Order(1)
@Slf4j
public class CreateRoles implements CommandLineRunner {

  @Autowired
  private RoleRepository roleRepository;

  @Override
  public void run(String... args) throws Exception {
    if (roleRepository.count() == 0) {
      Role adminRole = Role.builder().name("admin").build();
      Role customerRole = Role.builder().name("customer").build();
      roleRepository.save(adminRole);
      roleRepository.save(customerRole);
      log.info(">>>> Created admin and customer roles...");
    }
  }
}

我们首先使用 @Autowired 注解注入 RoleRepository 的实例。由于我们不希望在每次服务器重启时都创建角色,因此我们的逻辑只会在 RoleRepository 没有 Role 时执行。如果没有 Roles,那么我们使用 Lombok 构建器创建“admin”和“customer”角色。然后我们使用 RoleRepository save 方法将它们保存到 Redis。为了正确地记录消息,我们将使用 Lombok 提供的 @Slf4j(Java 的简单日志门面)注解,它创建了一个名为 log 的日志记录器实例,并具有常用的日志级别日志记录方法。在服务器启动时,我们现在应该在我们的数据库种子文件中看到一次输出。

2021-04-02 19:28:25.367  INFO 94971 --- [  restartedMain] c.r.edu.redi2read.Redi2readApplication   : Started Redi2readApplication in 2.146 seconds (JVM running for 2.544)
2021-04-02 19:28:25.654  INFO 94971 --- [  restartedMain] c.r.edu.redi2read.boot.CreateRoles       : >>>> Created admin and customer roles...

让我们使用 Redis CLI 来探索 Role 是如何存储的,让我们使用 KEYS 命令,传入 Role 的完全限定类名和通配符。结果是

127.0.0.1:6379> KEYS com.redislabs.edu.redi2read.models.Role*
1) "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
2) "com.redislabs.edu.redi2read.models.Role:9d383baf-35a0-4d20-8296-eedc4bea134a"
3) "com.redislabs.edu.redi2read.models.Role"

前两个值是 Hash,实际上是 Role 类的实例。 : 后面的字符串是单个 Role 的主键。让我们检查其中一个哈希

127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
hash
127.0.0.1:6379> HGETALL "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
1) "_class"
2) "com.redislabs.edu.redi2read.models.Role"
3) "id"
4) "c4219654-0b79-4ee6-b928-cb75909c4464"
5) "name"
6) "admin"

使用 TYPE 命令返回,正如预期的那样,键下的值为 Redis Hash。我们使用 HGETALL 来“获取所有”哈希中的值。 _class 是一个元数据字段,它标记存储在 Hash 中的对象的类。现在让我们检查 KEYS 列表中的第三个值

127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.Role"
set
127.0.0.1:6379> SMEMBERS "com.redislabs.edu.redi2read.models.Role"
1) "9d383baf-35a0-4d20-8296-eedc4bea134a"
2) "c4219654-0b79-4ee6-b928-cb75909c4464"

映射的类名下的 Redis Set 用于维护给定类的主键。