DDD-九、使用 Spring Data 发布领域事件
从 Spring 的角度来看,域事件只是另一个应用程序事件,可以使用内置的ApplicationEventPublisher
. 换句话说,我们不需要担心构建事件总线或其他一些基础设施来发布域事件:您将事件发布者注入到您的域服务中并发布事件。但是,在大多数情况下,您希望直接从聚合中发布域事件,而不必为此目的通过域服务。幸运的是,我们可以做到这一点。
Spring Data 提供了一种机制,可以直接从聚合中发布域事件,而无需获取事件发布者。我们在研究时已经触及了这种机制BaseAggregateRoot
,现在我们将仔细研究它。
在后台,Spring Boot 将为所有名称以save开头的存储库方法注册一个方法拦截器,例如save
and saveAndFlush
。此拦截器将在您的聚合中查找两种方法:一种带有注释的方法@DomainEvents
,另一种带有注释的方法@AfterDomainEventPublication
。
带有注释的方法@DomainEvents
预计会返回要发布的事件列表。拦截器将使用 Spring 应用程序事件发布者发布这些事件。@AfterDomainEventPublication
发布事件后,将调用带有注释的方法。此方法有望清除事件列表,以防止在下次保存聚合时再次发布它们。
以这种方式设计和发布域事件时要记住一个警告,它与包含对聚合根本身的引用的事件有关,例如:
public class PotentiallyProblematicDomainEvent {
private final MyAggregate myAggregate;
public PotentiallyProblematicDomainEvent(@NotNull MyAggregate myAggregate) {
this.myAggregate = myAggregate;
}
public @NotNull MyAggregate getMyAggregate() {
return myAggregate;
}
}
每当您设计这样的事件时,您必须了解 Spring Data 和 JPA 如何在后台工作。
当您保存现有实体时(这里我说的是 JPA 实体概念,而不是 DDD 实体概念),Spring Data 最终会调用EntityManager.merge
. 如果实体被分离,JPA 将检索托管实体,将所有属性从分离实体复制到托管实体,保存并返回。托管实体将增加其乐观锁定版本,而分离实体保持不变。
但是,由于域事件是在分离实体上注册的,因此域事件侦听器将获得对分离实体的引用。如果侦听器尝试直接在实体上执行任何操作然后保存它,这可能会导致乐观锁定错误。
以下是一些侦听器最终会收到带有不正确乐观锁定版本的陈旧实体的示例:
public class PotentiallyProblematicApplicationService {
@Transactional
public void firstProblematicMethod(@NotNull MyAggregate aggregate) { // <1>
aggregate.performAnOperationThatRegistersAProblematicDomainEvent();
myAggregateRepository.saveAndFlush(aggregate);
}
public void secondProblematicMethod(@NotNull MyAggregateId aggregateId) { // <2>
var aggregate = myAggregateRepository.getById(aggregateId);
aggregate.performAnOperationThatRegistersAProblematicDomainEvent();
myAggregateRepository.saveAndFlush(aggregate);
}
}
- 此方法接受聚合作为参数并直接对其执行操作。这意味着聚合是分离的,一旦保存就会变得陈旧。
- 此方法接受聚合 ID 作为参数,并在对其执行操作之前查找聚合。但是,该方法并不
@Transactional
意味着对存储库的两个调用都将在它们自己的事务中运行,从而在它们之间分离聚合。
现在我们如何解决这个问题?第二种方法很容易修复:只需制作整个方法@Transactional
。这样,在保存聚合时仍将对其进行管理,并且域事件侦听器将获得正确的实例。
但是第一种方法呢?一个明显的解决方案是在事件中使用聚合 ID 而不是聚合本身。但是,这有其自身的问题:如果事件在聚合被持久化之前注册,聚合没有 ID。如果您从未从非持久聚合中发布任何事件,这不是问题。但是,如果你这样做了,你可以像这样修复它:
public class SaferDomainEvent {
private final MyAggregate myAggregate;
public SaferDomainEvent(@NotNull MyAggregate myAggregate) { // <1>
this.myAggregate = myAggregate;
}
public @NotNull MyAggregateId getMyAggregateId() { // <2>
return myAggregate.getIdentifier();
}
}
- 我们仍然在事件中存储对聚合的引用……
- …但我们只将其 ID 暴露给外界,如果他们想对它做任何事情,则强制任何侦听器从存储库中获取聚合的新副本。
这又是一个底层技术悄悄潜入你的领域模型的例子——你选择的持久性技术甚至会影响你的领域事件的设计。幸运的是,在这种情况下,这没什么大不了的,但是如果您将早期设计建立在后来被证明是不正确的假设上,它仍然可能会在以后回来并咬您一口(我一直在那里做过,尤其是当涉及到 JPA 时)。底线是您需要充分了解您的工具——不仅要了解如何使用它们,还要了解它们是如何工作的。
- 原文作者:知识铺
- 原文链接:https://geek.zshipu.com/post/DDD/DDD-%E4%B9%9D%E4%BD%BF%E7%94%A8-Spring-%E5%A4%84%E7%90%86%E9%A2%86%E5%9F%9F%E4%BA%8B%E4%BB%B6/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com