使用 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 Aggregate 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 参考文档)。

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

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

其次,来自应用程序服务的非常具体的需求可能会潜入存储库,一段时间后,您的存储库会充满查询方法,这些方法几乎可以完成相同的工作,但变化很小。我想保持我的域模型尽可能干净。相反,我喜欢使用规范来构建我的查询。

当您按规范查询时,您首先要构建一个规范对象,该对象描述您想要从查询中得到的结果。规范对象也可以使用逻辑运算符andor进行组合。为了获得最大的灵活性,我尽量保持我的规格尽可能小。如果需要,我会为常用的规格组合创建复合规格。

Spring Data 内置了对规范的支持。要创建规范,您必须实现Specification接口。这个接口依赖于 JPA Criteria API,所以如果你以前没有使用过它,你需要熟悉它(这里是 Hibernate 的关于它的文档)。

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 实现(例如Hibernate)生成的元模型类。

然后,您将按以下方式使用规范:

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 更舒服,那么您没有理由改变。

规格和测试

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

但是,有一些方法可以解决这个问题。最明显的方法是在单元测试中模拟存储库时忽略对传入规范的检查,并使用单独的集成测试来测试你的规范,例如使用内存中的 H2 数据库。在许多情况下,这可能已经足够好了。