データ]


1概要

このチュートリアルでは、さまざまなテクノロジを使用してhttps://martinfowler.com/bliki/DDD__Aggregate.html[DDD Aggregates]を永続化する可能性を探ります。

** 2集合体の紹介

**

  • 集合体は、常に一貫している必要があるビジネスオブジェクトのグループです。したがって、トランザクション内で集計をまとめて保存および更新します。

集約はDDDにおける重要な戦術パターンであり、これは私たちのビジネスオブジェクトの一貫性を維持するのに役立ちます。ただし、集計という概念は、DDDのコンテキスト外でも役立ちます。

このパターンが役立つ可能性がある多くのビジネスケースがあります。

  • 経験則として、同じトランザクションの一部として変更された複数のオブジェクトがある場合は、集計の使用を検討してください。

注文の購入をモデル化するときにこれをどのように適用できるかを見てみましょう。

2.1. 発注書の例

では、発注書をモデル化したいとしましょう。

class Order {
    private Collection<OrderLine> orderLines;
    private Money totalCost;
   //...
}

class OrderLine {
    private Product product;
    private int quantity;
   //...
}

class Product {
    private Money price;
   //...
}

これらのクラスは単純な集合体を形成します。

Order



orderLines



totalCost

の両方のフィールドは常に一貫している必要があります。つまり、

totalCost

は常にすべての

orderLines

の合計に等しい値を持つ必要があります。

  • しかし、

    Order

    に単純なゲッターとセッターを導入すると、モデルのカプセル化が容易に破られ、ビジネス上の制約に違反する可能性があります。

何が悪くなるのか見てみましょう。

2.2. 素朴な集約デザイン


setOrderTotal

を含む

Order

クラスのすべてのプロパティに単純にゲッターとセッターを追加することにした場合に何が起こるかを想像してみましょう。

次のコードを実行するのを妨げるものは何もありません。

Order order = new Order();
order.setOrderLines(Arrays.asList(orderLine0, orderLine1));
order.setTotalCost(Money.zero(CurrencyUnit.USD));//this doesn't look good...

このコードでは、

totalCost

プロパティを手動でゼロに設定しているため、重要なビジネスルールに違反しています。確かに、総コストは0ドルであってはいけません!

  • 私たちはビジネスルールを守る方法を必要としています。集約ルートがどのように役立つかを見てみましょう。**

2.3. 集約ルート


集合体ルート

は、集合体へのエントリポイントとして機能するクラスです。

すべての業務はルートを通過する必要があります

このようにして、集約ルートは集約を一貫した状態に保つように注意を払うことができます。

  • 根本は、私たちのすべてのビジネス上の不変条件を処理することです。

そして、この例では、

Order

クラスが集約ルートの正しい候補です。集計が常に一貫していることを確認するために、いくつか修正を加えるだけです。

class Order {
    private final List<OrderLine> orderLines;
    private Money totalCost;

    Order(List<OrderLine> orderLines) {
        checkNotNull(orderLines);
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one order line item");
        }
        this.orderLines = new ArrayList<>(orderLines);
        totalCost = calculateTotalCost();
    }

    void addLineItem(OrderLine orderLine) {
        checkNotNull(orderLine);
        orderLines.add(orderLine);
        totalCost = totalCost.plus(orderLine.cost());
    }

    void removeLineItem(int line) {
        OrderLine removedLine = orderLines.remove(line);
        totalCost = totalCost.minus(removedLine.cost());
    }

    Money totalCost() {
        return totalCost;
    }

   //...
}

集約ルートを使用すると、

Product



OrderLine

を、すべてのプロパティが最終的な不変オブジェクトに簡単に変えることができます。

ご覧のとおり、これはかなり単純な集計です。

また、フィールドを使用せずに毎回単純に合計コストを計算することもできます。

しかし、今のところ私たちは集合体の設計ではなく集合体の永続性について話しています。この特定のドメインはすぐに役立つので、しばらくお待ちください。

これはパーシスタンステクノロジとどの程度うまく連携していますか?見てみましょう。

最終的に、これは私たちの次のプロジェクトに適した永続化ツールを選ぶのに役立ちます

3. JPAと休止状態

このセクションでは、JPAとHibernateを使って

Order

アグリゲートを試してみましょう。 Spring Bootとhttps://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-starter-data-jpa[JPA]スターターを使用します。

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

私たちのほとんどにとって、これは最も自然な選択のようです。結局のところ、私たちはリレーショナルシステムを扱うのに何年も費やしてきました、そして私たちは皆人気のあるORMフレームワークを知っています。

  • おそらくORMフレームワークを扱うときの最大の問題は、私たちのモデル設計の単純化です** 。

    https://en.wikipedia.org/wiki/Object-relational


    impedance

    mismatch[オブジェクト – リレーショナルインピーダンスの不一致]とも呼ばれます。

    Order

    集計を保持したい場合にどうなるかを考えてみましょう。

@DisplayName("given order with two line items, when persist, then order is saved")
@Test
public void test() throws Exception {
   //given
    JpaOrder order = prepareTestOrderWithTwoLineItems();

   //when
    JpaOrder savedOrder = repository.save(order);

   //then
    JpaOrder foundOrder = repository.findById(savedOrder.getId())
      .get();
    assertThat(foundOrder.getOrderLines()).hasSize(2);
}

この時点で、このテストは例外をスローします。__java.lang.IllegalArgumentException:不明なエンティティ

com.baeldung.ddd.order.Order__。

明らかに、JPAの要件の一部が欠けています。

  1. マッピング注釈を追加する


  2. OrderLine

    および

    Product

    クラスはエンティティまたは

    @ Embeddable

    である必要があります

単純な値オブジェクトではなくクラス
。各エンティティまたは

@ Embeddable

クラスに空のコンストラクタを追加する


  1. Money

    プロパティを単純型に置き換えます

    • うーん、JPAを使用できるように

      Order

      集計の設計を変更する必要があります。注釈を追加することは大した問題ではありませんが、他の要件によって多くの問題が発生する可能性があります。

3.1. 値オブジェクトへの変更

集約をJPAに適合させようとする最初の問題は、値オブジェクトの設計を破る必要があることです。それらの特性はもはや最終的なものではなくなり、カプセル化を破る必要があります。

  • これらのクラスが識別子を持つように設計されていない場合でも、

    OrderLine

    と__Productに人工IDを追加する必要があります** 。それらを単純な値オブジェクトにしたかったのです。

代わりに

@ Embedded

および

@ ElementCollection

アノテーションを使用することは可能ですが、この方法では、複雑なオブジェクトグラフを使用するときに非常に複雑になる可能性があります(たとえば、別の

@ Embedded

プロパティを持つ

@ Embeddable

オブジェクトなど)。


@ Embedded

アノテーションを使用すると、単純に親テーブルにフラットプロパティが追加されます。それ以外の基本プロパティ(

String

タイプなど)には、まだsetterメソッドが必要で、これは目的の値オブジェクトの設計に違反します。

  • 空のコンストラクタの要件は、値オブジェクトのプロパティがもう最終的ではないことを強制します。これは、私たちのオリジナルデザインの重要な側面を壊します。

実のところ、Hibernateはプライベートな引数のないコンストラクタを使用することができます。これにより、問題が少し軽減されますが、まだ完璧とは言えません。

プライベートデフォルトコンストラクタを使用している場合でも、プロパティをfinalとしてマークできないか、またはデフォルトコンストラクタ内でデフォルト(通常はnull)値で初期化する必要があります。

ただし、完全にJPA準拠にしたい場合は、デフォルトのコンストラクタに対して少なくとも保護された可視性を使用する必要があります。つまり、同じパッケージ内の他のクラスは、プロパティの値を指定せずに値オブジェクトを作成できます。

3.2. 複合型

  • 残念ながら、JPAがサードパーティの複合型をテーブルに自動的にマッピングすることは期待できません。前のセクションで導入しなければならなかった変更の数を確認してください。

たとえば、

Order

集計を使用して作業しているときに、

Joda Money

フィールドを維持するのが困難になることがあります。

そのような場合、JPA 2.1から利用可能なカスタム型

@ Converter

を書くことになるかもしれません。ただし、それには追加の作業が必要になるかもしれません。

あるいは、

Money

プロパティを2つの基本プロパティに分割することもできます。たとえば、通貨単位の場合は

String

、実績値の場合は

BigDecimal

です。

実装の詳細を隠してもパブリックメソッドAPIを介して

Money

クラスを使用することはできますが、ほとんどの開発者は余分な作業を正当化できず、代わりにJPA仕様に準拠するようにモデルを縮退します。

3.3. 結論

JPAは世界で最も採用されている仕様の1つですが、

Order

集合体を永続化するための最良の選択肢ではないかもしれません。

私たちのモデルに真のビジネスルールを反映させたいのであれば、基礎となるテーブルを単純な1:1で表現しないように設計する必要があります。

基本的に、ここでは3つの選択肢があります。

  1. 一連の単純なデータクラスを作成し、それらを永続化するために使用します.

豊富なビジネスモデルを作り直す。残念ながら、これには余分な作業が必要になるかもしれません。

  1. JPAの制限を受け入れて、適切な妥協点を選択してください.

  2. 別の技術を検討してください.

最初の選択肢は最大の可能性を秘めています。実際には、ほとんどのプロジェクトは2番目の方法で開発されています。

それでは、集計を維持するための別のテクノロジについて考えてみましょう。

4.ドキュメントストア

ドキュメントストアは、データを保存するための代替方法です。リレーションやテーブルを使う代わりに、オブジェクト全体を保存します。

これは、ドキュメントストアを永続的な集合体

の潜在的に完璧な候補にします。

このチュートリアルでは、JSONのような文書に焦点を当てます。

MongoDBのようなドキュメントストアでの順序永続性の問題がどのように見えるかを詳しく見てみましょう。

4.1. MongoDBを使用した集計の永続化

  • 今では、JSONデータを格納できるデータベースがかなり多くなっています(MongoDBが一般的です)** MongoDBは実際にはBSON、つまりJSONをバイナリ形式で格納します。

  • MongoDBのおかげで、

    Order

    サンプル集合体

    as-is

    を保存することができます。**

先に進む前に、Spring Boot

MongoDB

スターターを追加しましょう。

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

これでJPAの例と同様のテストケースを実行できますが、今回はMongoDBを使用します。

@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")
@Test
void test() throws Exception {
   //given
    Order order = prepareTestOrderWithTwoLineItems();

   //when
    repo.save(order);

   //then
    List<Order> foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List<OrderLine> foundOrderLines = foundOrders.iterator()
      .next()
      .getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}


  • 重要なこと – 元の

    Order

    集計クラスをまったく変更していません。

    **

    Money

    クラス用のデフォルトコンストラクタ、セッタ、またはカスタムコンバータを作成する必要はありません。

そして、これが

Order

集計がストアに表示される内容です。

{
  "__id": ObjectId("5bd8535c81c04529f54acd14"),
  "orderLines":[    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "10.00"
          }
        }
      },
      "quantity": 2
    },
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "5.00"
          }
        }
      },
      "quantity": 10
    }
 ],
  "totalCost": {
    "money": {
      "currency": {
        "code": "USD",
        "numericCode": 840,
        "decimalPlaces": 2
      },
      "amount": "70.00"
    }
  },
  "__class": "com.baeldung.ddd.order.mongo.Order"
}

この単純なBSONドキュメントは全体の

Order

集計を1つのピースにまとめたもので、これはすべて一貫して一貫しているべきだという私たちの当初の概念とうまく一致しています。

BSON文書内の複雑なオブジェクトは、通常のJSONプロパティのセットとして単純にシリアル化されていることに注意してください。このおかげで、(

Joda Money

のような)サードパーティーのクラスでさえ、モデルを単純化する必要なしに簡単にシリアライズすることができます。

4.2. 結論

MongoDBを使用してアグリゲートを永続化することはJPAを使用するよりも簡単です。

  • これは絶対にMongoDBが伝統的なデータベースより優れているという意味ではありません。

それでも、複雑な要件に従って常に一貫している必要があるオブジェクトのグループを特定した場合は、ドキュメントストアを使用することが非常に魅力的な選択肢になります。


5結論

DDDでは、集合体には通常、システム内で最も複雑なオブジェクトが含まれています。それらを扱うには、ほとんどのCRUDアプリケーションとはまったく異なるアプローチが必要です。

一般的なORMソリューションを使用すると、単純化された、または過度に公開されたドメインモデルにつながる可能性があります。

  • ドキュメントストアを使用すると、モデルの複雑さを犠牲にすることなく集合体を永続化しやすくなります。

すべての例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/ddd[GitHubで利用可能]です。