使用Spring Data JPA在更改實體時釋出DDD領域事件 - thorben

banq發表於2021-10-27

從 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對其進行註釋。

 

相關文章