持続するDDD集合体

DDD集計の永続化

1. 概要

このチュートリアルでは、さまざまなテクノロジーを使用してDDD Aggregatesを永続化する可能性を探ります。

2. 骨材の紹介

An aggregate is a group of business objects which always need to be consistent。 したがって、トランザクション内で集計を全体として保存および更新します。

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

このパターンが役立つビジネスケースは数多くあります。 As a rule of thumb, we should consider using aggregates when there are multiple objects changed as part of the same transaction

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

2.1. 注文書の例

したがって、注文書をモデル化するとします。

class Order {
    private Collection orderLines;
    private Money totalCost;
    // ...
}
class OrderLine {
    private Product product;
    private int quantity;
    // ...
}
class Product {
    private Money price;
    // ...
}

These classes form a simple aggregateOrderorderLinesフィールドとtotalCostフィールドはどちらも常に一貫している必要があります。つまり、totalCostは常にすべてのorderLinesの合計に等しい値である必要があります。

Now, we all might be tempted to turn all of these into fully-fledged Java Beans.ただし、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プロパティを手動でゼロに設定し、重要なビジネスルールに違反しています。 間違いなく、総コストはゼロドルであってはなりません!

ビジネスルールを保護する方法が必要です。 AggregateRootsがどのように役立つかを見てみましょう。

2.3. 集約ルート

aggregate rootは、アグリゲートへのエントリポイントとして機能するクラスです。 All business operations should go through the root.このようにして、アグリゲートルートはアグリゲートを一貫した状態に保つことができます。

The root is what takes cares of all our business invariants

この例では、Orderクラスが集約ルートの適切な候補です。 集約が常に一貫していることを確認するために、いくつかの変更を行う必要があります。

class Order {
    private final List orderLines;
    private Money totalCost;

    Order(List 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;
    }

    // ...
}

集約ルートを使用すると、ProductOrderLineを不変オブジェクトに簡単に変換できるようになりました。すべてのプロパティは最終的なものです。

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

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

ただし、現時点では、集計デザインではなく、集計の永続性についてのみ説明しています。 この特定のドメインがすぐに役立つので、お楽しみに。

これは永続化テクノロジーとどれだけうまく機能しますか? 見てみましょう。 Ultimately, this will help us to choose the right persistence tool for our next project

3. JPAおよびHibernate

このセクションでは、JPAとHibernateを使用してOrderの集計を永続化してみましょう。 Spring BootとJPAスターターを使用します。


    org.springframework.boot
    spring-boot-starter-data-jpa

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

Probably the biggest problem when working with ORM frameworks is the simplification of our model designObject-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: Unknown entity: com.example.ddd.order.OrderObviously, we’re missing some of the JPA requirements:

  1. マッピングアノテーションを追加する

  2. OrderLineおよびProductクラスは、単純な値オブジェクトではなく、エンティティまたは@Embeddableクラスである必要があります

  3. 各エンティティまたは@Embeddableクラスに空のコンストラクターを追加します

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

うーん、JPAを使用できるようにするには、Orderアグリゲートの設計を変更する必要があります。 注釈を追加することは大したことではありませんが、他の要件は多くの問題を引き起こす可能性があります。

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

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

We need to add artificial ids to the OrderLine and Product, even if these classes were never designed to have identifiers。 それらを単純な値オブジェクトにしたかったのです。

代わりに@Embeddedおよび@ElementCollectionアノテーションを使用することは可能ですが、このアプローチは、複雑なオブジェクトグラフ(たとえば、別の@Embeddedプロパティを持つ@Embeddableオブジェクトなど)を使用する場合に非常に複雑になる可能性があります。 。)。

@Embeddedアノテーションを使用すると、親テーブルにフラットプロパティが追加されるだけです。 それ以外は、基本的なプロパティ(例: Stringタイプの)でもsetterメソッドが必要であり、これは目的の値オブジェクトの設計に違反します。

Empty constructor requirement forces the value object properties to not be final anymore, breaking an important aspect of our original design。 正直なところ、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. 一連の単純なデータクラスを作成し、それらを使用して、リッチビジネスモデルを永続化および再作成します。 残念ながら、これには多くの追加作業が必要になる場合があります。

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

  3. 別のテクノロジーを検討してください。

最初のオプションには最大の可能性があります。 実際には、ほとんどのプロジェクトは2番目のオプションを使用して開発されます。

それでは、集計を永続化する別のテクノロジーについて考えてみましょう。

4. ドキュメントストア

ドキュメントストアは、データを保存する別の方法です。 リレーションとテーブルを使用する代わりに、オブジェクト全体を保存します。 This makes a document store a potentially perfect candidate for persisting aggregates

このチュートリアルのニーズのために、JSONのようなドキュメントに焦点を当てます。

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

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

Now, there are quite a few databases which can store JSON data, one of the popular being MongoDB. MongoDBは、実際にはBSONまたはJSONをバイナリ形式で格納します。

MongoDBのおかげで、Orderのサンプル集計as-isを保存できます。

先に進む前に、Spring BootMongoDBスターターを追加しましょう。


    org.springframework.boot
    spring-boot-starter-data-mongodb

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 foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List foundOrderLines = foundOrders.iterator()
      .next()
      .getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}

What’s important – we didn’t change the original Order aggregate classes at all; は、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.example.ddd.order.mongo.Order"
}

この単純なBSONドキュメントには、Orderの集計全体が1つのピースに含まれており、これらすべてが一緒に一貫している必要があるという元の概念とうまく一致しています。

BSONドキュメントの複雑なオブジェクトは、通常のJSONプロパティのセットとして単純にシリアル化されることに注意してください。 このおかげで、サードパーティのクラス(Joda Moneyなど)でも、モデルを単純化することなく簡単にシリアル化できます。

4.2. 結論

MongoDBを使用した集約の永続化は、JPAを使用するよりも簡単です。

This absolutely doesn’t mean MongoDB is superior to traditional databases.クラスを集計としてモデル化し、代わりにSQLデータベースを使用するべきではない正当なケースがたくさんあります。

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

5. 結論

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

一般的なORMソリューションを使用すると、複雑なビジネスルールを表現したり強制したりできないことが多い、単純化されたまたは過度に露出されたドメインモデルにつながる可能性があります。

ドキュメントストアを使用すると、モデルの複雑さを犠牲にすることなく、集計を簡単に永続化できます。

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