精通Spring Boot 3 : 6. 使用 Spring Boot 的 Spring Data NoSQL (1)

在本章中,我们将讨论 NoSQL 数据库,特别是 MongoDB 和 Redis。目前有许多 NoSQL 数据库可供选择,但 MongoDB 和 Redis 是 IT 行业中最常用的。Spring 框架的数据访问和 Spring Data 项目支持多种 NoSQL 数据库,您可以在 Spring Data 网站上找到每种技术的专用页面,包括 SQL 和 NoSQL:https://spring.io/projects/spring-data。

Spring Data MongoDB

Spring Data MongoDB 实现了 MongoDB 文档类型数据库的 Spring 编程模型。Spring Data MongoDB 的一个重要特性是它提供了 POJO(普通旧 Java 对象)模型,使得通过众所周知的仓库接口与集合进行交互变得更加简单。

以下是 Spring Data MongoDB 的一些主要功能:

  • 通过 JavaConfig 类或 XML 配置文件进行全面配置
  • 众所周知的 Spring 数据访问中的数据异常管理翻译
  • 生命周期中的回调和事件
  • 存储库、CrudRepository 和 MongoRepository 接口的实现方式
  • 自定义查询方法与 Querydsl 的集成
  • MapReduce 的集成。
  • 基于注解的映射元数据(使用@Document 注解指定您的领域类,使用@Id 注解指定标识符或键)
  • MongoTemplate 和 MongoOperations 类,帮助处理连接 MongoDB 和执行操作所需的所有样板代码
  • 通过 MongoReader 和 MongoWriter 抽象实现低级映射

基于 Spring Boot 的 Spring Data MongoDB 使用指南

如果您希望在 Spring Boot 中使用 MongoDB,您需要添加 spring-boot-starter-data-mongodb 启动依赖。Spring Boot 的自动配置会为连接 MongoDB 数据库设置所有必要的默认值,并识别您应用程序所需的所有仓库和领域类。

此自动配置将使用默认 URI mongodb://localhost/test 设置 MongoDatabaseFactory。您可以使用注入的 MongoTemplate 和 MongoOperations 类,如果您想更改这些默认设置,可以通过使用 spring.data.mongodb.* 属性来修改此行为。

基于 Spring Data MongoDB 和 Spring Boot 的用户应用程序

如果你在跟着做,可以重用一些之前版本的代码。或者,你可以从头开始,访问 Spring Initializr (https://start.spring.io) 生成一个基础项目(不带依赖),但请确保将 Group 字段设置为 com.apress,将 Artifact 和 Name 字段设置为 users。下载项目,解压缩后导入到你喜欢的 IDE 中。在本节结束时,你应该能够看到如图 6-1 所示的结构。


打开 build.gradle 文件,并将其内容替换为列表 6-1 中的内容。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    // Web
    implementation 'org.webjars:bootstrap:5.2.3'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
    useJUnitPlatform()
}
test {
    testLogging {
        events "passed", "skipped", "failed"
        showExceptions true
        exceptionFormat "full"
        showCauses true
        showStackTraces true
        showStandardStreams = false
    }
}

6-1 build.gradle

请注意,我们包含了 spring-boot-starter-data-mongodb 和 spring-boot-docker-compose 启动依赖项。Spring Boot 的自动配置功能将使用默认设置来配置所有内容,以便连接到 MongoDB 引擎并执行所需的操作和交互。

接下来,创建或打开用户类;它应该与清单 6-2 相似。

package com.apress.users;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Collection;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Document
public class User {
    @Id
    @NotBlank(message = "Email can not be empty")
    private String email;
    @NotBlank(message = "Name can not be empty")
    private String name;
    private String gravatarUrl;
    @Pattern(message = "Password must be at least 8 characters long and contain at least one number, one uppercase, one lowercase and one special character",
            regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!])(?=\\S+$).{8,}#34;)
    private String password;
    @Singular("role")
    private Collection<UserRole> userRole;
    private boolean active;
}

6-2 src/main/java/apress/com/users/User.java

你知道与用户类的前一个版本相比发生了什么变化吗(在 JPA 中,如清单 5-19 所示)?我们现在使用的是属于 org.springframework.data.mongo.*包的@Document 注解(而不是@Entity)。@Document 注解将该类标识为域对象,以便可以将其持久化到 MongoDB 数据库中。实际上就是这样。

接下来,创建或打开 UserRepository 接口。请查看清单 6-3。

package com.apress.users;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User,String>{}

6-3 src/main/java/apress/com/users/UserRepository.java

如你所见,这个类没有任何变化,我们依然继承自 CrudRepository 接口。虽然有 MongoRepository,但我们将在 My Retro App 项目中使用它。这里的目的是向你展示,当从一种技术迁移到另一种技术时,你仍然可以轻松使用你的基础。

接下来,创建或打开 UsersController 类。请参阅清单 6-4。

package com.apress.users;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor
@RestController
@RequestMapping("/users")
public class UsersController {
    private UserRepository userRepository;
    @GetMapping
    public ResponseEntity<Iterable<User>> getAll(){
        return ResponseEntity.ok(this.userRepository.findAll());
    }
    @GetMapping("/{email}")
    public ResponseEntity<User> findUserById(@PathVariable String email) throws Throwable {
        return ResponseEntity.of(this.userRepository.findById(email));
    }
    @RequestMapping(method = {RequestMethod.POST,RequestMethod.PUT})
    public ResponseEntity<User> save(@RequestBody User user){
       this.userRepository.save(user);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{email}")
                .buildAndExpand(user.getEmail())
                .toUri();
        return ResponseEntity.created(location).body(user);
    }
    @DeleteMapping("/{email}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void save(@PathVariable String email){
        this.userRepository.deleteById(email);
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}

6-4 src/main/java/apress/com/users/UsersController.java

UsersController 类没有变化,依然和之前的版本一样。不过请注意,我们可以直接使用由 Spring 注入的 UserRepository(作为构造函数的一部分,这要感谢 Lombok 的 @AllArgsConstructor 注解)。

接下来,创建或打开 UserConfiguration 类。请参阅清单 6-5。

package com.apress.users;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import java.util.Arrays;
@Configuration
public class UserConfiguration implements BeforeConvertCallback<User> {
    @Bean
    ApplicationListener<ApplicationReadyEvent> init(UserRepository userRepository){
        return applicationReadyEvent -> {
            userRepository.save(User.builder()
                    .email("ximena@email.com")
                    .name("Ximena")
                    .password("aw2s0meR!")
                    .role(UserRole.USER)
                    .active(true)
                    .build());
            userRepository.save(User.builder()
                    .email("norma@email.com")
                    .name("Norma")
                    .password("aw2s0meR!")
                    .role(UserRole.USER)
                    .role(UserRole.ADMIN)
                    .active(true)
                    .build());
        };
    }
    @Override
    public User onBeforeConvert(User entity, String collection) {
        if (entity.getGravatarUrl()==null)
            entity.setGravatarUrl(UserGravatar.getGravatarUrlFromEmail(entity.getEmail()));
        if (entity.getUserRole() == null)
            entity.setUserRole(Arrays.asList(UserRole.INFO));
        return entity;
    }
}

6-5 src/main/java/apress/com/users/UserConfiguration.java

UserConfiguration 类实现了 BeforeConvertCallback 接口,您可以在实体保存到 MongoDB 数据库之前添加自定义逻辑。您需要实现 onBeforeConvert 方法。请注意,您可以直接在配置类中添加必要的实现,而无需为回调事件创建另一个类。此外,该类使用 ApplicationReadyEvent 初始化了一些文档(与之前的版本相同),并使用 UserRepository 接口。

接下来,创建或打开 docker-compose.yaml 文件。请参阅清单 6-6。

version: "3.1"
services:
  mongo:
    image: mongo
    restart: always
    environment:
      MONGO_INITDB_DATABASE: retrodb
    ports:
      - "27017:27017"

6-6 docker-compose.yaml

docker-compose.yaml 文件确保在运行应用程序时,您的 MongoDB 数据库能够正常工作。请注意,此文件仅用于开发环境(而非测试环境)。

我们在这里已经完成了。UserGravatar 和 UserRole 类没有任何更改。application.properties 文件必须保持空白;我们不需要在其中指定任何内容。

用户应用测试

要测试用户应用程序,请创建或打开 UsersHttpRequestTests 类。请参见第 6-7 号列表。

package com.apress.users;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {"spring.data.mongodb.database=retrodb"})
public class UsersHttpRequestTests {
    @Value("${local.server.port}")
    private int port;
    private final String BASE_URL = "http://localhost:";
    private final String USERS_PATH = "/users";
    @Autowired
    private TestRestTemplate restTemplate;
    @Test
    public void indexPageShouldReturnHeaderOneContent(){
        assertThat(this.restTemplate.getForObject(BASE_URL + port,
                String.class)).contains("Simple Users Rest Application");
    }
    @Test
    public void usersEndPointShouldReturnCollectionWithTwoUsers() {
        Collection response = this.restTemplate.
                getForObject(BASE_URL + port + USERS_PATH, Collection.class);
        assertThat(response.size()).isEqualTo(2);
    }
    @Test
    public void userEndPointPostNewUserShouldReturnUser(){
        User user =  User.builder()
                .email("dummy@email.com")
                .name("Dummy")
                .gravatarUrl("https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar")
                .password("aw2s0meR!")
                .role(UserRole.USER)
                .active(true)
                .build();
        User response =  this.restTemplate.postForObject(BASE_URL + port + USERS_PATH,user,User.class);
        assertThat(response).isNotNull();
        assertThat(response.getEmail()).isEqualTo(user.getEmail());
        Collection users = this.restTemplate.
                getForObject(BASE_URL + port + USERS_PATH, Collection.class);
        assertThat(users.size()).isGreaterThanOrEqualTo(2);
    }
    @Test
    public void userEndPointDeleteUserShouldReturnVoid() {
        this.restTemplate.delete(BASE_URL + port + USERS_PATH + "/norma@email.com");
        Collection users = this.restTemplate.
                getForObject(BASE_URL + port + USERS_PATH, Collection.class);
        assertThat(users.size()).isLessThanOrEqualTo(2);
    }
    @Test
    public void userEndPointFindUserShouldReturnUser() {
        User user = this.restTemplate.getForObject(BASE_URL + port + USERS_PATH + "/ximena@email.com",User.class);
        assertThat(user).isNotNull();
        assertThat(user.getEmail()).isEqualTo("ximena@email.com");
    }
}

6-7 src/test/java/apress/com/users/UsersHttpRequestTests.java

UsersHttpRequestTests 类与之前的版本相同,但这里有一个有趣的地方需要注意。在前面的部分,我们将 application.properties 文件留空。实际上,我们添加的 spring-boot-docker-compose 依赖仅适用于开发,因此要使用 Spring Boot 的大多数默认设置,最低要求是添加我们将要使用的数据库名称(retrodb);否则,名称将默认为 test。

要运行测试,您首先需要启动 MongoDB,您可以通过 IDE(VS Code 和 IntelliJ 都有运行 docker-compose 文件的插件)或命令行来完成此操作:

docker compose up -d

该命令将使用 docker-compose 依赖项在后台启动 mongodb 服务。接下来,您可以进行测试。

./gradlew clean test

你应该得到如下输出:

UsersHttpRequestTests > userEndPointFindUserShouldReturnUser() PASSED
UsersHttpRequestTests > userEndPointDeleteUserShouldReturnVoid() PASSED
UsersHttpRequestTests > indexPageShouldReturnHeaderOneContent() PASSED
UsersHttpRequestTests > userEndPointPostNewUserShouldReturnUser() PASSED
UsersHttpRequestTests > usersEndPointShouldReturnCollectionWithTwoUsers() PASSED

你现在可以停止 Docker Compose 了

docker compose down

有这个 Docker Compose 功能作为测试支持会很不错,对吧?你将在测试章节(第 8 章)中学习如何使用测试容器来实现这一点。

启动用户应用程序

要运行用户应用,请确保您没有启动 docker compose——用户应用会为您处理这个。请使用以下命令:

./gradlew clean bootRun
...
...

输出显示容器已成功启动。现在,请打开另一个终端并使用以下命令:

curl -s http://localhost:8080/users  |  jq .
  {
    "email": "ximena@email.com",
    "name": "Ximena",
    "gravatarUrl": "https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar",
    "password": "aw2s0meR!",
    "userRole": [
      "USER"
    ],
    "active": true
  },
  {
    "email": "dummy@email.com",
    "name": "Dummy",
    "gravatarUrl": "https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar",
    "password": "aw2s0meR!",
    "userRole": [
      "USER"
    ],
    "active": true
  },
  {
    "email": "norma@email.com",
    "name": "Norma",
    "gravatarUrl": "https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar",
    "password": "aw2s0meR!",
    "userRole": [
      "USER",
      "ADMIN"
    ],
    "active": true
  }
]
# MongoDB
spring.data.mongodb.uri=mongodb://retroadmin:aw2s0me@other-server:27017/retrodb?directConnection=true&serverSelectionTimeoutMS=2000&authSource=admin&appName=mongosh+1.7.1
spring.data.mongodb.database=retrodb

mongodb URI 的格式如下:

mongodb://<username>:<password>@<remote-server>:<port>/<database>[?...]

在连接 URL 中,您必须将用户名和密码作为参数之一进行指定,因为授权源的默认值是 admin。请与您的管理员核对您的 URI 详细信息。