学习

对象映射与 Redis 仓库

Brian Sam-Bodden
作者
Brian Sam-Bodden Redis 开发者布道师

目标#

配置 RedisTemplate 并学习如何从 Spring REST 控制器访问不同的操作捆绑包以读写 Redis 数据。

议程#

在本课程中,您将学习

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

如果您遇到困难

用户和角色#

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

了解 @RedisHash#

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

Role 模型

让我们从创建领域中最简单的类 Role 类开始,该类位于目录 src/main/java/com/redislabs/edu/redi2read/models 下。我们将文件命名为 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 类型的 timeToLive 参数和一个 String 值,后者将覆盖默认的 Redis 键前缀(默认情况下,键前缀是类的完全限定名加上一个冒号)。

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

User 模型​

User 模型将用于注册/signup 方法。为了进行服务器端验证,我们需要在 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 Hashes 持久化 (@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,并限制其 @Size
  6. 6.passwordConfirm 字段将用于传统的“确认密码”,该字段被标记为 @Transient,以便 @RedisHash 不会尝试将其序列化到数据库中
  7. 7.对于 User 的角色,我们有一个 Role 对象的 Set 集合,这些对象被标记为 @References,这将导致它们作为给定角色的 id 存储在支持 User 实例的 Redis Hash 中。
  8. 8.最后在底部,我们添加了一个实用方法,用于将 Role 添加到 User 的 Role Set 中。

Spring 仓库#

现在我们已经正确标注了两个模型,我们需要关联的仓库来对这些模型进行数据操作。Spring Data 仓库类似于 DAO (数据访问对象) 模式,因为它们都抽象了对底层数据存储的操作。区别在于,仓库通过专注于对象集合的管理来进一步抽象底层存储机制,而 DAO 更以 SQL/表为中心。

Role 仓库#

在目录 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

测试 Role 仓库#

让我们通过使用 CommandLineRunner 实现来测试 RoleRepositoryCommandLineRunner 是一个接口,它告诉 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 时执行。如果没有 Role,那么我们就使用 Lombok 的 builder 创建“admin”和“customer”角色。然后使用 RoleRepository 的 save 方法将它们保存到 Redis。为了正确记录消息,我们将使用 Lombok 提供的 @Slf4j (Simple Logging Facade for Java) 注解,它会创建一个名为 log 的 logger 实例,包含常用的日志级别记录方法。服务器启动时,我们应该会看到一次数据库填充的输出。

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 的主键。让我们检查其中一个 Hash

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 命令来获取 Hash 中的所有值。 _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 用于维护给定类的主键。