使用Spring Data JPA在更改实体时发布DDD领域事件 - thorben


从 Spring Data JPA 1.11(Ingalls 版本)开始,您可以在保存实体对象时自动发布域事件。您只需要向实体类添加一个方法,该方法返回要发布的事件对象的 集合 ,并使用@DomainEvents注释该方法 。Spring Data JPA 调用该方法并在您执行 实体存储库的save 或 saveAll方法时发布事件 。与其他 Spring 应用程序事件类似,您可以使用@EventListener或@TransactionalEventListener观察它们。
此实现的主要目标是支持领域驱动设计DDD中定义的领域事件。这些通常由聚合根发布,用于通知应用程序的其他部分您的业务领域中发生了事件。与其他常用事件(如实体生命周期事件)相比,领域事件不应包含任何技术细节。
当然,您可以使用 Spring 的ApplicationEventPublisher在业务代码中以编程方式发布这些事件。如果事件是由特定业务操作而不是属性值的更改触发的,那么这通常是正确的方法。但是如果不同的业务操作导致实体对象发生相同的变化并触发相同的事件,则使用领域事件更容易且不易出错。
 
从实体类发布域事件
如前所述,您的实体类必须提供一个用@DomainEvents注释的方法,该方法返回您要发布的所有事件。每个事件由一个对象表示。我建议为要触发的每种类型的事件使用特定的类。这使得更容易实现仅对特定类型的事件做出反应的事件观察。
在本文的示例中,我想在锦标赛结束时发布域事件。我创建了TournamentEndedEvent类来表示这个事件。它包含锦标赛的 ID 及其结束日期。

public class TournamentEndedEvent {
 
    private Long tournamentId;
 
    private LocalDate endDate;
 
    public TournamentEndedEvent(Long tournamentId, LocalDate endDate) {
        this.tournamentId = tournamentId;
    }
 
    public Long getTournamentId() {
        return tournamentId;
    }
 
    public LocalDate getEndDate() {
        return endDate;
    }
}

自己实现事件发布:
告诉 Spring Data JPA 您要发布哪些事件的一种选择是实现您自己的方法并使用@DomainEvents对其进行 注释。
在我的ChessTournament类的endTournament方法中,我将比赛的endDate设置为now。然后我实例化一个新的TournamentEndedEvent并将其添加到我想在保存锦标赛时发布的事件列表中。
@Entity
public class ChessTournament {
 
    @Transient
    private final List<Object> domainEvents = new ArrayList<>();
 
    private LocalDate endDate;
 
    // more entity attributes
     
    public void endTournament() {
        endDate = LocalDate.now();
        domainEvents.add(new TournamentEndedEvent(id, endDate));
    }
 
    @DomainEvents
    public List<Object> domainEvents() {
        return domainEvents;
    }
 
    @AfterDomainEventPublication
    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

正如您在代码片段中看到的,我还实现了 2 个额外的方法。
我注释的domainEvents方法与@DomainEvents注释和返回列表我要发布的事件。这就是我之前提到的方法。Spring Data JPA 在我调用ChessTournamentRepository上的save或saveAll方法时调用它。
clearDomainEvents方法上标注@AfterDomainEventPublication告诉Spring Data JPA 在domainEvents方法返回的所有事件后调用此方法。根据您的观察者实现,这可以在您的观察者中处理事件之前或之后的事情。
在本例中,我使用
clearDomainEvents
方法清除事件列表。这确保我不会两次发布任何事件,即使我的业务代码多次调用我的ChessTournamentRepository的save方法。
 
扩展 Spring 的AbstractAggregateRoot
正如您在上一节中看到的,您可以轻松实现所需的方法来管理要发布的事件列表并将其提供给 Spring Data JPA。但我建议使用更简单的选项。
Spring Data 提供了AbstractAggregateRoot类,它为您提供了所有这些方法。您只需要扩展它并调用registerEvent方法将您的事件对象添加到List。
@Entity
public class ChessTournament extends AbstractAggregateRoot<ChessTournament> {
 
    private LocalDate endDate;
 
    // more entity attributes
     
    public void endTournament() {
        endDate = LocalDate.now();
        registerEvent(new TournamentEndedEvent(id, endDate));
    }
}

观察领域事件
Spring 提供了强大的事件处理机制,在Spring 文档中有详细解释。您可以以与任何其他 Spring 事件相同的方式观察域事件。在本文中,我将快速概述 Spring 的事件处理特性,并指出在事务上下文中工作时的一些陷阱。
要实现观察者,您需要实现一个方法,该方法需要事件类类型的 1 个参数,并使用@EventListener或@TransactionalEventListener对其进行注释。

  • 同步观察事件

Spring 在事件发布者的事务上下文中同步执行所有用@EventListener注释的观察者 。只要您的观察者使用 Spring Data JPA,它的所有读写操作都使用与触发事件的业务代码相同的上下文。这使其能够读取当前事务的未提交更改并将其自己的更改添加到其中。
在下面的观察器执行,我用它来改变 结束 所有标志 ChessGame一个第 ChessTournament 到 真正的 和写一个简短的日志信息。
@EventListener
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
    log.info("===== Handling TournamentEndedEvent ====");
 
    Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
    chessTournament.ifPresent(tournament -> {
        tournament.getGames().forEach(chessGame -> {
            chessGame.setEnded(true);
            log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
        });
    });
}

让我们在下面的测试用例中使用这个事件观察器和之前描述的 ChessTournament 实体。它从数据库中获取一个ChessTournament实体并调用该实体的endTournament方法。然后它调用锦标赛存储库的保存方法并在之后写入日志消息。

log.info("===== Test Domain Events =====");
ChessTournament chessTournament = tournamentRepository.getOne(1L);
 
// End the tournament
chessTournament.endTournament();
 
// Save the tournament and trigger the domain event
ChessTournament savedTournament = tournamentRepository.save(chessTournament);
log.info("After tournamentRepository.save(chessTournament);");

您可以在日志输出中看到 Spring Data JPA 在保存实体时调用了事件观察器。这是一个同步调用,它暂停了测试用例的执行,直到所有观察者都处理了事件。观察者执行的所有操作都是当前事务的一部分。这使观察者能够初始化从ChessTournament到ChessGame实体的延迟获取的关联,并更改每个游戏的结束属性。


2021-10-23 14:56:33.158  INFO 10352 - – [           main] c.t.janssen.spring.data.TestKeyConcepts  : ===== Test Domain Events =====
2021-10-23 14:56:33.180 DEBUG 10352 - – [           main] org.hibernate.SQL                        : select chesstourn0_.id as id1_2_0_, chesstourn0_.end_date as end_date2_2_0_, chesstourn0_.name as name3_2_0_, chesstourn0_.start_date as start_da4_2_0_, chesstourn0_.version as version5_2_0_ from chess_tournament chesstourn0_ where chesstourn0_.id=?
2021-10-23 14:56:33.216  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : ===== Handling TournamentEndedEvent ====
2021-10-23 14:56:33.221 DEBUG 10352 - – [           main] org.hibernate.SQL                        : select games0_.chess_tournament_id as chess_to6_0_0_, games0_.id as id1_0_0_, games0_.id as id1_0_1_, games0_.chess_tournament_id as chess_to6_0_1_, games0_.date as date2_0_1_, games0_.ended as ended3_0_1_, games0_.player_black_id as player_b7_0_1_, games0_.player_white_id as player_w8_0_1_, games0_.round as round4_0_1_, games0_.version as version5_0_1_ from chess_game games0_ where games0_.chess_tournament_id=?
2021-10-23 14:56:33.229  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 3 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 2 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 5 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 1 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 6 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 4 ended: true
2021-10-23 14:56:33.230  INFO 10352 - – [           main] c.t.janssen.spring.data.TestKeyConcepts  : After tournamentRepository.save(chessTournament);
2021-10-23 14:56:33.283 DEBUG 10352 - – [           main] org.hibernate.SQL                        : update chess_tournament set end_date=?, name=?, start_date=?, version=? where id=? and version=?
2021-10-23 14:56:33.290 DEBUG 10352 - – [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?
2021-10-23 14:56:33.294 DEBUG 10352 - – [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?
2021-10-23 14:56:33.296 DEBUG 10352 - – [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?

  • 观察事务结束时的事件

如果要在当前事务结束时执行观察者,则需要使用@TransactionalEventListener而不是@EventListener对其进行注释。Spring 然后在定义的TransactionPhase 中调用观察者。您可以在BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK和AFTER_COMPLETION之间进行选择。默认情况下,Spring 在AFTER_COMMIT阶段执行事务观察者。
除了不同的注解之外,您还可以采用与我在上一个示例中向您展示的同步观察器相同的方式来实现您的事件观察器。
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
    log.info("===== Handling TournamentEndedEvent ====");
 
    Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
    chessTournament.ifPresent(tournament -> {
        tournament.getGames().forEach(chessGame -> {
            chessGame.setEnded(true);
            log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
        });
    });
}

在这种情况下,我决定在 Spring 提交事务之前执行我的观察者。这确保观察者不会阻止我的测试用例的执行。当 Spring 调用观察者时,事务上下文仍然处于活动状态,所有执行的操作都成为我的测试用例启动的事务的一部分。
当我执行与前一个示例相同的测试用例时,您可以在日志输出中看到 Spring 在我的测试用例执行其所有操作之后但在 Spring 提交事务之前调用了观察者。
 
使用领域事件时的陷阱
处理领域事件看起来很简单,但有几个陷阱会导致 Spring 不发布事件、不调用观察者或不持久化观察者执行的更改。
  • 无保存调用 = 无事件

如果您在其存储库上调用save或saveAll方法,Spring Data JPA 仅发布实体的域事件。
但是,如果您正在使用托管实体(通常是您在当前事务期间从数据库中获取的每个实体对象),您就不需要调用任何存储库方法来持久化您的更改。您只需要在实体对象上调用 setter 方法并更改属性的值。您的持久性提供程序,例如 Hibernate,会自动检测更改并保持不变。
  • 无事务 = 无事务观察者

如果您提交或回滚事务,Spring 只会调用我在第二个示例中向您展示的事务观察者。如果您的业务代码在没有活动事务的情况下发布事件,则 Spring 不会调用这些观察者。
AFTER_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION = 需要新事务
如果您实现一个事务观察者并将其附加到事务阶段AFTER_COMMIT、AFTER_ROLLBACK或AFTER_COMPLETION,Spring 将在没有活动事务的情况下执行观察者。因此,您只能从数据库读取数据,但 Spring Data JPA 不会保留任何更改。
您可以通过使用@Transactional(propagation = Propagation.REQUIRES_NEW)注释您的观察者方法来避免该问题。这告诉 Spring Data JPA 在调用观察者之前启动一个新事务并在之后提交它。
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
    log.info("===== Handling TournamentEndedEvent ====");
 
    Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
    chessTournament.ifPresent(tournament -> {
        tournament.getGames().forEach(chessGame -> {
            chessGame.setEnded(true);
            log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
        });
    });
}

这样做时,请记住观察者的事务独立于触发事件的业务代码所使用的事务。
  • BEFORE_COMMIT = 修改

如果你将你的事件观察者附加到BEFORE_COMMIT事务阶段,就像我在前面的一个例子中所做的那样,Spring 会执行观察者作为你当前事务的一部分。因此,您不能保证所有修改或更改都已刷新到数据库,并且只有在使用相同事务访问数据库时才能看到刷新的更改。
为了防止您的观察者处理过时的信息,您应该使用 Spring Data JPA 的存储库来访问您的数据库。这就是我在本文的示例中所做的。它使您可以访问当前持久性上下文中所有未刷新的更改,并确保您的查询是同一事务的一部分。
  
结论
领域事件,如领域驱动设计中所定义,描述了在您的应用程序的业务领域中发生的事件。
使用 Spring Data JPA,您可以在调用存储库的save或saveAll方法时发布一个或多个域事件。Spring 然后检查所提供的实体是否具有使用@DomainEvents注释的方法,调用它,并发布返回的事件对象。
您可以采用与 Spring 中的任何其他事件观察器相同的方式为您的域事件实现观察器。您只需要一个需要事件类类型参数的方法,并使用@EventListener或@TransactionalEventListener对其进行注释。