1. 概要

このチュートリアルでは、@DomainEventsアノテーションとAbstractAggregateRootクラスを使用して、ドメインの主要な戦術設計パターンの1つであるaggregateによって生成されたドメインイベントを便利に公開および処理する方法について説明します。駆動設計。

アグリゲートはビジネスコマンドを受け入れます。これにより、通常、ビジネスドメインに関連するイベント(ドメインイベント)が生成されます。

DDDと集計について詳しく知りたい場合は、EricEvansのオリジナルの本から始めるのが最善です。 VaughnVernonによって書かれた効果的な骨材設計に関する素晴らしいシリーズもあります。 間違いなく読む価値があります。

ドメインイベントを手動で操作するのは面倒な場合があります。 ありがたいことに、 Spring Frameworkを使用すると、データリポジトリを使用して集約ルートを操作するときに、ドメインイベントを簡単に公開および処理できます。

2. Mavenの依存関係

Spring Dataは、Ingallsリリーストレインに@DomainEventsを導入しました。 あらゆる種類のリポジトリで利用できます。

この記事で提供されるコードサンプルは、SpringDataJPAを使用しています。 Springドメインイベントをプロジェクトに統合する最も簡単な方法は、Spring Boot Data JPA Starterを使用することです:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

3. イベントを手動で公開する

まず、ドメインイベントを手動で公開してみましょう。 次のセクションでは、@DomainEventsの使用法について説明します。

この記事のニーズに応じて、ドメインイベントに空のマーカークラスDomainEventを使用します。

標準のApplicationEventPublisherインターフェースを使用します。

イベントを公開できる2つの良い場所があります。サービスレイヤーまたはアグリゲート内に直接

3.1. サービスレイヤー

サービスメソッド内でリポジトリ保存メソッドを呼び出した後、イベントを簡単に公開できます。

サービスメソッドがトランザクションの一部であり、 @TransactionalEventListener で注釈が付けられたリスナー内のイベントを処理する場合、イベントはトランザクションが正常にコミットされた後にのみ処理されます。

したがって、トランザクションがロールバックされ、集計が更新されない場合に、「偽の」イベントが処理されるリスクはありません。

@Service
public class DomainService {
 
    // ...
    @Transactional
    public void serviceDomainOperation(long entityId) {
        repository.findById(entityId)
            .ifPresent(entity -> {
                entity.domainOperation();
                repository.save(entity);
                eventPublisher.publishEvent(new DomainEvent());
            });
    }
}

これは、イベントが実際にservice DomainOperationによって公開されていることを証明するテストです。

@DisplayName("given existing aggregate,"
    + " when do domain operation on service,"
    + " then domain event is published")
@Test
void serviceEventsTest() {
    Aggregate existingDomainEntity = new Aggregate(1, eventPublisher);
    repository.save(existingDomainEntity);

    // when
    domainService.serviceDomainOperation(existingDomainEntity.getId());

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

3.2. 集計

アグリゲート内から直接イベントを公開することもできます。

このようにして、クラス内でのドメインイベントの作成を管理します。これは、次のように自然に感じられます。

@Entity
class Aggregate {
    // ...
    void domainOperation() {
        // some business logic
        if (eventPublisher != null) {
            eventPublisher.publishEvent(new DomainEvent());
        }
    }
}

残念ながら、Spring Dataがリポジトリからエンティティを初期化する方法が原因で、これは期待どおりに機能しない可能性があります。

実際の動作を示す対応するテストは次のとおりです。

@DisplayName("given existing aggregate,"
    + " when do domain operation directly on aggregate,"
    + " then domain event is NOT published")
@Test
void aggregateEventsTest() {
    Aggregate existingDomainEntity = new Aggregate(0, eventPublisher);
    repository.save(existingDomainEntity);

    // when
    repository.findById(existingDomainEntity.getId())
      .get()
      .domainOperation();

    // then
    verifyNoInteractions(eventHandler);
}

ご覧のとおり、イベントはまったく公開されていません。 アグリゲート内に依存関係を持たせるのは良い考えではないかもしれません。 この例では、ApplicationEventPublisherはSpringDataによって自動的に初期化されません。

アグリゲートは、デフォルトのコンストラクターを呼び出すことによって構築されます。 期待どおりに動作させるには、エンティティを手動で再作成する必要があります(例: カスタムファクトリまたはアスペクトプログラミングを使用)。

また、aggregateメソッドが終了した直後にイベントを公開することは避けてください。 少なくとも、100 % s ureでない限り、このメソッドはトランザクションの一部です。 そうしないと、変更がまだ永続化されていないときに「偽の」イベントが公開される可能性があります。 これにより、システムに不整合が生じる可能性があります。

これを回避したい場合は、トランザクション内で常に集計メソッドを呼び出すことを忘れないでください。 残念ながら、この方法では、設計を永続化テクノロジーに強く結び付けています。 トランザクションシステムを常に使用しているとは限らないことを覚えておく必要があります。

したがって、一般的には、アグリゲートでドメインイベントのコレクションを管理し、永続化されようとしているときにそれらを返すようにすることをお勧めします

次のセクションでは、@DomainEventsおよび@AfterDomainEvents アノテーションを使用して、ドメインイベントの公開をより管理しやすくする方法について説明します。

4. @DomainEventsを使用してイベントを公開する

Spring Data Ingallsリリーストレイン以降、@DomainEventsアノテーションを使用してドメインイベントを自動的に公開できます

@DomainEvents で注釈が付けられたメソッドは、エンティティが適切なリポジトリを使用して保存されるたびに、SpringDataによって自動的に呼び出されます。

次に、このメソッドによって返されるイベントは、ApplicationEventPublisherインターフェイスを使用して公開されます。

@Entity
public class Aggregate2 {
 
    @Transient
    private final Collection<DomainEvent> domainEvents;
    // ...
    public void domainOperation() {
        // some domain operation
        domainEvents.add(new DomainEvent());
    }

    @DomainEvents
    public Collection<DomainEvent> events() {
        return domainEvents;
    }
}

この動作を説明する例を次に示します。

@DisplayName("given aggregate with @DomainEvents,"
    + " when do domain operation and save,"
    + " then event is published")
@Test
void domainEvents() {
 
    // given
    Aggregate2 aggregate = new Aggregate2();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

ドメインイベントが公開された後、@AfterDomainEventsPublicationで注釈が付けられたメソッドが呼び出されます。

このメソッドの目的は通常、すべてのイベントのリストをクリアすることであるため、将来、それらが再度公開されることはありません。

@AfterDomainEventPublication
public void clearEvents() {
    domainEvents.clear();
}

このメソッドをAggregate2クラスに追加して、どのように機能するかを見てみましょう。

@DisplayName("given aggregate with @AfterDomainEventPublication,"
    + " when do domain operation and save twice,"
    + " then an event is published only for the first time")
@Test
void afterDomainEvents() {
 
    // given
    Aggregate2 aggregate = new Aggregate2();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

イベントが初めて公開されることがはっきりとわかります。clearEventsメソッドから@AfterDomainEventPublicationアノテーションを削除すると、同じイベントが2回目に公開されます

ただし、実際に何が起こるかは実装者次第です。 Springは、このメソッドの呼び出しのみを保証します。それ以上のことはありません。

5. AbstractAggregateRootテンプレートを使用する

AbstractAggregateRootテンプレートクラスのおかげで、ドメインイベントの公開をさらに簡素化することができます。 新しいドメインイベントをイベントのコレクションに追加する場合は、registerメソッドを呼び出すだけです。

@Entity
public class Aggregate3 extends AbstractAggregateRoot<Aggregate3> {
    // ...
    public void domainOperation() {
        // some domain operation
        registerEvent(new DomainEvent());
    }
}

これは、前のセクションで示した例に対応しています。

すべてが期待どおりに機能することを確認するためだけに、テストは次のとおりです。

@DisplayName("given aggregate extending AbstractAggregateRoot,"
    + " when do domain operation and save twice,"
    + " then an event is published only for the first time")
@Test
void afterDomainEvents() {
 
    // given
    Aggregate3 aggregate = new Aggregate3();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

@DisplayName("given aggregate extending AbstractAggregateRoot,"
    + " when do domain operation and save,"
    + " then an event is published")
@Test
void domainEvents() {
    // given
    Aggregate3 aggregate = new Aggregate3();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

ご覧のとおり、生成するコードが大幅に少なくなり、まったく同じ効果が得られます。

6. 実装に関する警告

最初は@DomainEvents機能を使用するのは良い考えのように思えるかもしれませんが、注意が必要ないくつかの落とし穴があります。

6.1. 未発表のイベント

JPAを使用する場合、変更を永続化するときに必ずしもsaveメソッドを呼び出す必要はありません。

コードがトランザクションの一部である場合(例: @Transactional )で注釈を付け、既存のエンティティに変更を加えると、通常、リポジトリでsaveメソッドを明示的に呼び出さずにトランザクションをコミットさせます。 したがって、アグリゲートが新しいドメインイベントを生成したとしても、それらが公開されることはありません。

@DomainEvents機能は、SpringDataリポジトリを使用している場合にのみ機能することも覚えておく必要があります。 これは重要な設計要素になる可能性があります。

6.2. 失われたイベント

イベントの公開中に例外が発生した場合、リスナーに通知が届くことはありません

どういうわけかイベントリスナーの通知を保証できたとしても、現在、パブリッシャーに何か問題が発生したことを知らせるためのバックプレッシャーはありません。 イベントリスナーが例外によって中断された場合、イベントは消費されないままになり、再度公開されることはありません。

この設計上の欠陥は、Spring開発チームに知られています。 リード開発者の1人は、この問題に対する可能な解決策を提案しました。

6.3. ローカルコンテキスト

ドメインイベントは、単純なApplicationEventPublisherインターフェイスを使用して公開されます。

デフォルトでは、ApplicationEventPublisherを使用すると、イベントは同じスレッドで公開および消費されます。 すべてが同じコンテナで行われます。

通常、ある種のメッセージブローカーを介してイベントを送信するため、他の分散クライアント/システムに通知が届きます。 このような場合、イベントを手動でメッセージブローカーに転送する必要があります。

SpringIntegrationまたはApacheCamelなどのサードパーティソリューションを使用することもできます。

7. 結論

この記事では、@DomainEventsアノテーションを使用して集約ドメインイベントを管理する方法を学習しました。

このアプローチでは、イベントインフラストラクチャを大幅に簡素化できるため、ドメインロジックのみに焦点を当てることができます。 特効薬はなく、Springがドメインイベントを処理する方法も例外ではないことに注意する必要があります。

すべての例の完全なソースコードは、GitHubから入手できます。