DDD-六、使用 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 Aggregate getById(@NotNull ID id) { // <5>
return findById(id).orElseThrow(() -> new EmptyResultDataAccessException(1));
}
}
- 这个注解告诉 Spring Data 不要试图直接实例化这个接口。
- 我们将存储库服务的实体限制为仅聚合根。
- 我们扩展
JpaRepository
. - 我个人更喜欢规范而不是查询方法。我们稍后会回到为什么。
- 内置
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。
我这样做主要有两个原因:首先,方法名称往往会变得很长,并且无论在哪里使用都会污染代码。
其次,来自应用程序服务的非常具体的需求可能会潜入存储库,一段时间后,您的存储库会充满查询方法,这些方法几乎可以完成相同的工作,但变化很小。我想保持我的域模型尽可能干净。相反,我喜欢使用*规范*来构建我的查询。
当您按规范查询时,您首先要构建一个规范对象,该对象描述您想要从查询中得到的结果。规范对象也可以使用逻辑运算符*and*和*or*进行组合。为了获得最大的灵活性,我尽量保持我的规格尽可能小。如果需要,我会为常用的规格组合创建复合规格。
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));
}
}
- 这里我只是做一个简单的
like
查询,但在现实世界的规范中,您可能希望更彻底,注意通配符、大小写匹配等。 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
);
}
}
- 永远不要编写返回没有上限的结果集的方法(至少在生产代码中)。要么使用分页(就像我在这里做的那样),要么对查询可以返回的记录数使用有限且合理的限制。
- 这里使用运算符将两个规范组合在一起
and
。
关于存储库和 QueryDSL 的说明
Spring Data 还支持QueryDSL。在这种情况下,您不是使用规范,而是直接使用 QueryDSL 谓词。设计原则几乎相同,因此如果您觉得 QueryDSL 比 JPA Criteria API 更舒服,那么您没有理由改变。
规格和测试
使用规范来支持查询方法有一个明显的缺点,这与单元测试有关。Criteria
由于规范在后台使用了 JPA Criteria API,因此在不构造和分析其 JPA 谓词的情况下,没有简单的方法对给定对象的内容进行断言 - 这是一个重要的过程。
但是,有一些方法可以解决这个问题。最明显的方法是在单元测试中模拟存储库时忽略对传入规范的检查,并使用单独的集成测试来测试你的规范,例如使用内存中的 H2 数据库。在许多情况下,这可能已经足够好了。
- 原文作者:知识铺
- 原文链接:https://geek.zshipu.com/post/DDD-%E5%85%AD%E4%BD%BF%E7%94%A8-Spring-Data-%E6%9E%84%E5%BB%BA%E5%AD%98%E5%82%A8%E5%BA%93/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com