13.1.4 定义 OrderRepository 聚合根服务

将 TacoOrder 和 Taco 对象持久化在一起的第一步是使 TacoOrder 成为聚合根,将在 TacoOrder 类中添加一个 Taco 集合属性。如清单 13.8 所示。

清单 13.8 将 Tac o集合添加到 TacoOrder 中。

@Data
public class TacoOrder {

  ...

  @Transient
  private transient List<Taco> tacos = new ArrayList<>();

  public void addTaco(Taco taco) {
    this.tacos.add(taco);
    if (taco.getId() != null) {
      this.tacoIds.add(taco.getId());
    }
  }
}

除了将名为 tacos 的新 List<Taco> 属性添加到 TacoOrder 类之外,addTaco() 方法现在将给定的 Taco 添加到该列表中(以及将其 id 添加到 tacoIds 中)。

但是,请注意 tacos 属性加了 @Transient 注解(以及 Java 关键字 transient),这表明 Spring Data R2DBC 不应该持久化此字段。如果没有 @Transient 注释,Spring Data R2DBC 将尝试持久化,会由于不支持而导致错误。

保存 TacoOrder 时,只有 tacoIds 属性会写入数据库和 tacos 属性将被忽略。即使如此,至少现在 TacoOrder 有了存放 Taco 的地方。这对于保存 TacoOrder 时,同时保存 Taco 对象,以及获取 TacoOrder 时同时读入 Taco 对象,都方便很多。

现在,我们可以创建一个服务 bean 来保存和读取 TacoOrder 对象及其中的 Taco 对象。让我们从保存 TacoOrder 开始,清单 13.9 中定义的 TacoOrderAggregateService 类有一个 save() 方法正是这样做的。

清单 13.9 将 Tacoorder 和 Taco 聚合保存。

package tacos.web.api;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
import tacos.Taco;
import tacos.TacoOrder;
import tacos.data.OrderRepository;
import tacos.data.TacoRepository;

@Service
@RequiredArgsConstructor
public class TacoOrderAggregateService {

  private final TacoRepository tacoRepo;
  private final OrderRepository orderRepo;

  public Mono<TacoOrder> save(TacoOrder tacoOrder) {
    return Mono.just(tacoOrder)
      .flatMap(order -> {
        List<Taco> tacos = order.getTacos();
        order.setTacos(new ArrayList<>());
        return tacoRepo.saveAll(tacos)
            .map(taco -> {
              order.addTaco(taco);
              return order;
          }).last();
      })
      .flatMap(orderRepo::save);
  }
}

虽然清单 13.9 中的行不多,但 save() 方法中有很多内容需要解释。首先,包装作为参数接收的 TacoOrder 在 Mono 中使用 Mono.just() 方法。这使我们能够将其作为一种响应性类型,在 save() 方法所有地方直接使用。

接下来我们要做的是将一个 flatMap() 应用到我们刚刚创建的 Mono<TacoOrder> 上。map()flatMap() 是对通过 Mono 或 Flux 的数据对象进行转换的方法。由于我们在转换过程最终结果是 Mono<TacoOrder>flatMap() 操作确保我们继续使用 Mono<TacoOrder>,而如果我们改为使用 map(), Mono 映射后会是 Mono<Mono<TacoOrder>

映射的目的是保存这些 Taco 对象时,确保 TacoOrder 的 Taco 对象都最终只有 ID。一个新的 TacoOrder 对象,其中的每个 Taco 对象的 ID 可能为 null,直到 Taco 对象被保存之后,我们才知道 ID 的值。

从 TacoOrder 获取 List<Taco> 后,我们将在保存 Taco 对象时使用该列表,我们将 tacos 属性重置为空列表。我们将用新的 Taco 对象重建列表,使用保存后已分配的 ID 值。

对注入的 TacoRepository 调用 saveAll() 方法将保存所有 Taco 对象。saveAll() 方法返回一个 Flux<Taco>,然后通过 map() 循环处理。在这种情况下,转换操作是次要的,因为每个 Taco 对象会被添加回 TacoOrder。但是为了确保结果 Flux 是一个 TacoOrder 而不是一个 Taco,最后,映射操作返回的是 TacoOrder,而不是Taco。对 last() 的调用确保映射操作的结果不会有重复的 TacoOrder 对象(每个 Taco 对应一个)。

此时,应保存所有 Taco 对象,然后将其推回父对象 TacoOrder 对象,以及它们新分配的 ID。剩下的就是保存 TacoOrder,这就是最后的 flatMap() 调用所做的。再次使用 flatMap() 确保 OrderRepository.save() 返回的是 Mono<TacoOrder>,没有被包装在另一个 Mono 中,我们希望 save() 方法返回 Mono<TacoOrder> 而不是 Mono<Mono<TacoOrder>>

现在让我们来看一个方法,该方法将通过 TacoOrder 的 ID 读取 TacoOrder,并重新构造所有 Taco 对象。清单 13.10 显示了这个新的findById() 方法。

清单 13.10 将 TacoOrders 和 Tacos 聚合读取。

public Mono<TacoOrder> findById(Long id) {
  return orderRepo
    .findById(id)
    .flatMap(order -> {
      return tacoRepo.findAllById(order.getTacoIds())
        .map(taco -> {
          order.addTaco(taco);
          return order;
        }).last();
  });
}

findById() 方法比 save() 短一些。但些方法中仍然有很多需要说的点。

方法中做的第一件事是通过调用 OrderRepository 的 findById() 获取 TacoOrder。方法返回一个 Mono<TacoOrder>,然后通过扁平化映射对其进行变换,从只有 Taco ID 的 TacoOrder,变成包含完整 Taco 对象的 TacoOrder。

提供给 flatMap() 方法的 lambda 调用 TacoRepository.findAllById() 方法,获取 TacoIds 中引用的所有 Taco 对象。 这会产生一个 Flux<Taco>,它通过 map() 循环处理,将每个 Taco 添加到父对象 TacoOrder 中,就像我们在 save() 方法中所做的那样,使用 saveAll() 保存所有 Taco 对象。

同样,map() 操作更多地被用作对 Taco 对象进行迭代的一种方法,而不是作为一种转变。但是每次给 map() 的 lambda 都会返回父对象 TacoOrder。我们最终得到的是 Flux<TacoOrder>,而不是 Flux<Taco>。对 last() 的调用使用最后一项,并返回一个 Mono<TacoOrder>,这是 findById() 方法的返回值。

如果您还没有习惯响应式编程方式,save()findById() 方法中的代码可能让您感到迷惑。响应式编程需要一种不同的思维方式,而且有时令人困惑,但当您的响应式编程技巧提高以后,就会发现它非常优雅。

就像 TacoOrderAggregateService 中的代码,这可能让您感觉迷惑。所以编写测试以确保其正常工作是一个好主意期。该测试还将作为 TacoOrderAggregateService 如何运行的示例。清单 13.11 显示了对 TacoOrderAggregateService 的测试类。

清单 13.11 测试 TacoorderaggegateService。

package tacos.web.api;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.test.annotation.DirtiesContext;

import reactor.test.StepVerifier;
import tacos.Taco;
import tacos.TacoOrder;
import tacos.data.OrderRepository;
import tacos.data.TacoRepository;

@DataR2dbcTest
@DirtiesContext
public class TacoOrderAggregateServiceTests {

  @Autowired
  TacoRepository tacoRepo;

  @Autowired
  OrderRepository orderRepo;

  TacoOrderAggregateService service;

  @BeforeEach
  public void setup() {
    this.service = new TacoOrderAggregateService(tacoRepo, orderRepo);
  }

  @Test
  public void shouldSaveAndFetchOrders() {
    TacoOrder newOrder = new TacoOrder();
    newOrder.setDeliveryName("Test Customer");
    newOrder.setDeliveryStreet("1234 North Street");
    newOrder.setDeliveryCity("Notrees");
    newOrder.setDeliveryState("TX");
    newOrder.setDeliveryZip("79759");
    newOrder.setCcNumber("4111111111111111");
    newOrder.setCcExpiration("12/24");
    newOrder.setCcCVV("123");

    newOrder.addTaco(new Taco("Test Taco One"));
    newOrder.addTaco(new Taco("Test Taco Two"));

    StepVerifier.create(service.save(newOrder))
      .assertNext(this::assertOrder)
      .verifyComplete();
    StepVerifier.create(service.findById(1L))
      .assertNext(this::assertOrder)
      .verifyComplete();
  }
  private void assertOrder(TacoOrder savedOrder) {
    assertThat(savedOrder.getId()).isEqualTo(1L);
    assertThat(savedOrder.getDeliveryName()).isEqualTo("Test Customer");
    assertThat(savedOrder.getDeliveryName()).isEqualTo("Test Customer");
    assertThat(savedOrder.getDeliveryStreet()).isEqualTo("1234 North Street");
    assertThat(savedOrder.getDeliveryCity()).isEqualTo("Notrees");
    assertThat(savedOrder.getDeliveryState()).isEqualTo("TX");
    assertThat(savedOrder.getDeliveryZip()).isEqualTo("79759");
    assertThat(savedOrder.getCcNumber()).isEqualTo("4111111111111111");
    assertThat(savedOrder.getCcExpiration()).isEqualTo("12/24");
    assertThat(savedOrder.getCcCVV()).isEqualTo("123");
    assertThat(savedOrder.getTacoIds()).hasSize(2);
    assertThat(savedOrder.getTacos().get(0).getId()).isEqualTo(1L);
    assertThat(savedOrder.getTacos().get(0).getName())
        .isEqualTo("Test Taco One");
    assertThat(savedOrder.getTacos().get(1).getId()).isEqualTo(2L);
    assertThat(savedOrder.getTacos().get(1).getName())
        .isEqualTo("Test Taco Two");
  }
}

清单 13.11 中有很多行,但其中大部分是 assertOrder() 方法中的断言。此测试类,我们将重点关注其他部分。

测试类用了 @DataR2dbcTest 注解,以使 Spring 创建的应用程序上下文中包含相关 Repository 的 bean。@DataR2dbcTest 查找带有注解 @SpringBootConfiguration 的配置类,来定义 Spring应用程序上下文。在单模块项目中,引导类用 @SpringBootApplication 注解可以达到这个目的(它本身会有 @SpringBootConfiguration 注解)。但在我们的多模块项目中,测试类与引导类不在同一个项目中,因此我们需要一个简单的配置:

package tacos;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@SpringBootConfiguration
@EnableAutoConfiguration
public class TestConfig {

}

这不仅满足了 @SpringBootConfiguration 注解类的需要,而且还支持自动配置,确保Repository 被创建(还有许多其他功能)。

就其本身而言,TacoOrderAggregateServiceTests 应该可以通过测试。但是在 IDE 环境中,可能在测试运行之间共享 JVM 和 Spring 应用程序上下文,与其他测试一起运行此测试,可能会导致将冲突数据写入内存中的 H2 数据库。这个 @DirtiesContext 注解来确保重置Spring 应用程序上下文。在测试运行时,每次运行都会产生一个新的空 H2 数据库。

setup() 方法使用注入测试类的 TacoRepository 和 OrderRepository 对象,创建 TacoOrderAggregateService 实例。这个TacoOrderAggregateService 被分配给一个实例变量,以便测试方法可以使用它。

现在我们终于准备好测试我们的聚合服务了。前几行 shouldSaveAndFetchOrders() 构建一个 TacoOrder 对象,并加入了几个 Taco 对象。然后 TacoOrder 对象通过 TacoOrderAggregateService 的 save() 方法保存,返回 Mono<TacoOrder>,表示保存的顺序。使用StepVerifier,我们断言返回的 Mono 中的 TacoOrder 符合我们的期望,包括它包含的子 Taco 对象。

接下来,我们调用服务的 findById() 方法,该方法返回一个 Mono<TacoOrder>。像调用 save() 方法那样,一个 StepVerifier 用于遍历返回 Mono 中的每个 TacoOrder(应该只有一个)并声称它符合我们的期望。

在这两种 StepVerifier 情况下,对 verifyComplete() 的调用确保 Mono 中没有更多对象,并确认 Mono 已完成处理。

值得注意的是,尽管我们可以应用类似的聚合操作,来确保 Taco 对象总是包含完整的 Ingredient 对象。但我选择不这样做,Ingredient 是它自己的聚合根,可能被多个 Taco 对象引用。因此,每个 Taco 将只携带 Set<Long> 指向 Ingredient ID,然后可以 通过 IngredientRepository 单独查找。

尽管聚合实体可能需要更多的工作,但 Spring Data R2DBC 提供了以响应式方式处理关系数据的方式。但这并不是 Spring 提供的唯一的响应式持久化数据的选择。让我们看看如何使用 Spring Data Repostotry 响应式的使用 MongoDB。

results matching ""

    No results matching ""