DDD:使用Spring数据构建存储库

389 阅读5分钟

翻译自:dev.to/peholmst/bu…

前面我们学习了如何使用Spring Data构建聚合。现在,当我们有了汇总之后,我们需要构建存储库来存储和检索它们。

使用Spring Data构建存储库非常容易。您需要做的就是声明您的存储库接口,并使它扩展Spring Data接口JpaRepository。但是,这也使意外地为本地实体创建存储库变得容易(如果您的开发人员不熟悉DDD但熟悉JPA,则可能会发生这种情况)。因此,我总是这样声明自己的基本存储库接口:

@NoRepositoryBean // <1>
public interface BaseRepository<Aggregate extends BaseAggregateRoot<ID>, ID extends Serializable> // <2>
        extends JpaRepository<Aggregate, ID>,  // <3>
                JpaSpecificationExecutor<Aggregate> { // <4>

    default @NotNull T getById(@NotNull ID id) { // <5>
        return findById(id).orElseThrow(() -> new EmptyResultDataAccessException(1));
    }
}
  1. 这个注释告诉Spring Data不要尝试直接实例化该接口。
  2. 我们将存储库服务的实体限制为仅聚合根。
  3. 我们扩展JpaRepository。
  4. 我个人更喜欢规范而不是查询方法。我们稍后会返回到为什么。
  5. 内置findById方法返回Optional。在许多情况下,当您通过其ID获取聚合时,就假定该聚合将存在。不得不Optional每次处理都是浪费时间和代码,因此您最好直接在存储库中进行处理。

有了此基本接口后,Customer聚合根的存储库可能看起来像这样:

public interface CustomerRepository extends BaseRepository<Customer, CustomerId> {
    // No need for additional methods
}

这是检索和保存聚合所需的全部。现在让我们看一下如何实现查询。

查询方法和规格

在Spring Data中创建查询的最直接的方法是定义仔细命名的findBy-methods(如果您不熟悉此方法,请查看Spring Data参考文档

我发现这些对于仅基于一两个键查找聚集的简单查询很有用;例如,在中,PersonRepository您可以使用称为的方法findBySocialSecurityNumber;在中,CustomerRepository您可以使用称为的方法findByCustomerNumber。但是,对于更高级或更复杂的查询,我尽量避免使用findBy-methods。

我这样做主要有两个原因:首先,方法名称往往变得很长,并且无论使用什么地方都会污染代码。

其次,来自应用程序服务的非常具体的需求可能会潜入存储库中,不久后,您的存储库中就会充满执行几乎相同功能但变化很小的查询方法。我想保持域模型尽可能整洁。相反,我喜欢使用规格来构造查询。

当按规范查询时,首先要构建一个规范对象,该对象描述所需的查询结果。规范对象也可以使用逻辑运算符和和或进行组合。为了获得最大的灵活性,我尝试使规格尽可能小。如果需要,我为常用的规格组合创建复合规格。

Spring Data内置了对规范的支持。要创建规范,您必须实现Specification接口。该接口依赖于JPA Criteria API,因此如果您以前没有使用过它,则需要熟悉一下。

该Specification接口包含您必须实现的单个方法。它产生一个JPA Criteria谓词,并将创建该谓词所需的所有必要对象作为输入。

创建规格的最简单方法是建立规格工厂。最好用一个例子来说明:

public class CustomerSpecifications {

    public @NotNull Specification<Customer> byName(@NotNull String name) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like( // <1>
            root.get(Customer_.name), // <2>
            name
        );
    }

    public @NotNull Specification<Customer> byLastInvoiceDateAfter(@NotNull LocalDate date) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get(Customer_.lastInvoiceDate), date);
    }

    public @NotNull Specification<Customer> byLastInvoiceDateBefore(@NotNull LocalDate date) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Customer_.lastInvoiceDate), date);
    }

    public @NotNull Specification<Customer> activeOnly() {
        return (root, query, criteriaBuilder) -> criteriaBuilder.isTrue(root.get(Customer_.active));
    }
}
  1. 在这里,我只是做一个简单的like查询,但是在现实世界的规范中,您可能希望更全面,注意通配符,大小写匹配等。
  2. Customer_是由JPA实现生成的元模型类

然后,您可以通过以下方式使用规格:

public class CustomerService {

    private final CustomerRepository repository;
    private final CustomerSpecifications specifications;

    public CustomerService(CustomerRepository repository, CustomerSpecifications specifications) {
        this.repository = repository;
        this.specifications = specifications;
    }

    public Page<Customer> findActiveCustomersByName(String name, Pageable pageable) { // <1>
        return repository.findAll(
            specifications.byName(name).and(specifications.activeOnly()), // <2>
            pageable
        );
    }
}
  1. 永远不要编写返回没有上限的结果集的方法(至少在生产代码中)。使用分页(如我在此处所做的那样),或者对查询可以返回的记录数使用有限且合理的限制。
  2. 这里使用and操作符将两个规范组合在一起。

关于存储库和QueryDSL的说明

Spring Data还支持QueryDSL。在这种情况下,您不使用规范,而是直接使用QueryDSL谓词。设计原理几乎相同,因此,如果您对QueryDSL感到比对JPA Criteria API感到更舒服,那么就没有理由进行更改。

规格和测试

使用规范来支持查询方法存在一个明显的缺点,它与单元测试有关。由于规范是在后台使用JPA Criteria API,因此没有简单的方法就可以在Criteria不构造和分析其JPA谓词的情况下对给定对象的内容进行断言-这是一个不平凡的过程。

但是,有一些解决方法。最明显的方法是在单元测试中模拟存储库时仅忽略检查传入的规范,而使用单独的集成测试来测试您的规范(例如,使用内存中的H2数据库)。在许多情况下,这可能就足够了。

还有另一种方法可以避免使用集成测试,但是需要一些额外的前期工作。如果仔细看一下规范工厂,您会发现工厂方法不是静态的,但是实例方法和类本身不是最终的。这意味着您可以模拟或存根整个工厂。另外,由于工厂方法仅返回实现该Specification接口的对象,因此您也可以模拟或存根该接口。这意味着只要您避免在Specification接口(使用JPA Criteria API),您可以构建一个模拟规范工厂,该工厂返回模拟规范,然后可以对其进行分析并用作测试断言的基础。不幸的是,这篇文章不是深入了解此内容的合适位置,因此我将其作为练习留给读者。