次の方法で共有


インフラストラクチャの永続化レイヤーを設計する

ヒント

このコンテンツは、.NET Docs で入手できる、またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できる、コンテナー化された .NET アプリケーションの電子ブックである .NET マイクロサービス アーキテクチャからの抜粋です。

コンテナー化された .NET アプリケーションの .NET マイクロサービス アーキテクチャの電子ブックの表紙サムネイル。

データ永続化コンポーネントは、マイクロサービス (つまり、マイクロサービスのデータベース) の境界内でホストされているデータへのアクセスを提供します。 カスタム Entity Framework (EF) オブジェクトなど、リポジトリやDbContextクラスなどのコンポーネントの実際の実装が含まれています。 EF DbContext は、リポジトリと作業単位の両方のパターンを実装します。

リポジトリ パターン

リポジトリ パターンは、システムのドメイン モデルの外部で永続化の問題を維持することを目的とした Domain-Driven 設計パターンです。 1 つ以上の永続化抽象化 (インターフェイス) がドメイン モデルで定義されており、これらの抽象化には、アプリケーションの他の場所で定義された永続化固有のアダプターの形式で実装が含まれています。

リポジトリの実装は、データ ソースにアクセスするために必要なロジックをカプセル化するクラスです。 一般的なデータ アクセス機能を一元化することで、保守性が向上し、ドメイン モデルからデータベースにアクセスするために使用されるインフラストラクチャまたはテクノロジが分離されます。 Entity Framework のような Object-Relational マッパー (ORM) を使用する場合、LINQ と厳密な型指定により、実装する必要があるコードが簡略化されます。 これにより、データアクセスの設定ではなく、データ永続化ロジックに重点を置くことができます。

リポジトリ パターンは、データ ソースを操作するための適切に文書化された方法です。 Enterprise アプリケーション アーキテクチャのパターンに関する書籍では、Martin Fowler はリポジトリを次のように記述しています。

リポジトリは、ドメイン モデル レイヤーとデータ マッピングの間の仲介者のタスクを実行し、メモリ内のドメイン オブジェクトのセットと同様の方法で動作します。 クライアント オブジェクトは、宣言によってクエリを作成し、リポジトリに送信して回答を求めます。 概念的には、リポジトリはデータベースに格納されている一連のオブジェクトとそれらに対して実行できる操作をカプセル化し、永続化レイヤーに近い方法を提供します。 また、リポジトリは、作業ドメインとデータの割り当てまたはマッピング間の依存関係を明確かつ一方向に分離する目的をサポートします。

集計ごとに 1 つのリポジトリを定義する

集約または集約ルートごとに、1 つのリポジトリ クラスを作成する必要があります。 C# ジェネリックを利用して、保持する必要がある具象クラスの合計数を減らすことができます (この章で後述します)。 Domain-Driven Design (DDD) パターンに基づくマイクロサービスでは、データベースの更新に使用する必要がある唯一のチャネルがリポジトリである必要があります。 これは、集計ルートとの 1 対 1 のリレーションシップがあり、集計の不変性とトランザクションの一貫性を制御するためです。 クエリはデータベースの状態を変更しないため、(CQRS アプローチに従って行うことができるように) 他のチャネルを使用してデータベースにクエリを実行しても問題ありません。 ただし、トランザクション領域 (つまり更新) は、常にリポジトリと集約ルートによって制御される必要があります。

基本的に、リポジトリを使用すると、データベースから取得されたデータをドメイン エンティティの形式でメモリに設定できます。 エンティティがメモリ内に入ったら、エンティティを変更し、トランザクションを介してデータベースに永続化できます。

前述のように、CQS/CQRS アーキテクチャ パターンを使用している場合、初期クエリはドメイン モデル外のサイド クエリによって実行され、Dapper を使用した単純な SQL ステートメントによって実行されます。 この方法は、必要なテーブルのクエリと結合が可能であり、これらのクエリが集計のルールによって制限されないため、リポジトリよりもはるかに柔軟です。 そのデータは、プレゼンテーション レイヤーまたはクライアント アプリに送信されます。

ユーザーが変更を加えると、更新されるデータはクライアント アプリまたはプレゼンテーション レイヤーからアプリケーション レイヤー (Web API サービスなど) に送信されます。 コマンド ハンドラーでコマンドを受け取ると、リポジトリを使用して、データベースから更新するデータを取得します。 コマンドで渡されたデータを使用してメモリ内で更新し、トランザクションを使用してデータベース内のデータ (ドメイン エンティティ) を追加または更新します。

図 7-17 に示すように、各集約ルートに対して 1 つのリポジトリのみを定義する必要があることを再度強調することが重要です。 集計ルートの目標を達成して、集計内のすべてのオブジェクト間のトランザクション整合性を維持するには、データベース内の各テーブルのリポジトリを作成しないでください。

ドメインとその他のインフラストラクチャの関係を示す図。

図 7-17 リポジトリ、集計、およびデータベース テーブル間のリレーションシップ

上の図は、ドメインレイヤーとインフラストラクチャレイヤー間の関係を示しています。購入者集計は IBuyerRepository と Order Aggregate に依存し、IOrderRepository インターフェイスに依存します。これらのインターフェイスは、データ層のテーブルにアクセスする UnitOfWork に依存する対応するリポジトリによってインフラストラクチャ レイヤーに実装されています。

リポジトリごとに 1 つの集約ルートを適用する

集約ルートのみがリポジトリを持つ必要があるルールを適用するように、リポジトリ設計を実装することは有益です。 エンティティの種類を制約するジェネリック リポジトリまたはベース リポジトリ型を作成できます。このエンティティは IAggregateRoot マーカー インターフェイスを持っている必要があります。

したがって、インフラストラクチャ レイヤーで実装される各リポジトリ クラスは、次のコードに示すように、独自のコントラクトまたはインターフェイスを実装します。

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
      // ...
    }
}

各特定のリポジトリ インターフェイスは、汎用 IRepository インターフェイスを実装します。

public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);
    // ...
}

ただし、コードで各リポジトリが 1 つの集計に関連するという規則を適用するより良い方法は、ジェネリック リポジトリ型を実装することです。 そうすることで、リポジトリを使用して特定の集計をターゲットにすることが明示的になります。 これは、次のコードのように、汎用 IRepository 基本インターフェイスを実装することで簡単に行うことができます。

public interface IRepository<T> where T : IAggregateRoot
{
    //....
}

リポジトリ パターンを使用すると、アプリケーション ロジックを簡単にテストできます

リポジトリ パターンを使用すると、単体テストでアプリケーションを簡単にテストできます。 単体テストでは、インフラストラクチャではなくコードのみをテストするため、リポジトリの抽象化により、その目標を簡単に達成できます。

前のセクションで説明したように、Web API マイクロサービスなどのアプリケーション 層が実際のリポジトリ クラスを実装したインフラストラクチャ レイヤーに直接依存しないように、リポジトリ インターフェイスを定義してドメイン モデル レイヤーに配置することをお勧めします。 これを行い、Web API のコントローラーで依存関係の挿入を使用することで、データベースからのデータではなく偽のデータを返すモック リポジトリを実装できます。 この分離されたアプローチを使用すると、データベースへの接続を必要とせずに、アプリケーションのロジックに焦点を当てる単体テストを作成して実行できます。

データベースへの接続は失敗する可能性があり、さらに重要なことに、データベースに対して数百のテストを実行することは、2 つの理由で不適切です。 まず、テストの数が多いため、時間がかかる場合があります。 次に、データベース レコードが変更され、テストの結果に影響を与える可能性があります。特に、テストが並列で実行されている場合は、一貫性が得られない可能性があります。 単体テストは通常、並列で実行できます。統合テストでは、実装によっては並列実行がサポートされない場合があります。 データベースに対するテストは単体テストではなく、統合テストです。 多数の単体テストを高速に実行する必要がありますが、データベースに対する統合テストは少なくなります。

単体テストの懸念事項の分離に関しては、ロジックはメモリ内のドメイン エンティティで動作します。 リポジトリ クラスがそれらを配信していることを前提としています。 ロジックによってドメイン エンティティが変更されると、リポジトリ クラスによって正しく格納されると想定されます。 ここで重要なポイントは、ドメイン モデルとそのドメイン ロジックに対する単体テストを作成することです。 集約ルートは、DDD の主な整合性境界です。

eShopOnContainers に実装されているリポジトリは、変更トラッカーを使用して EF Core のリポジトリと作業単位パターンの DbContext 実装に依存するため、この機能は重複しません。

リポジトリ パターンと従来の Data Access クラス (DAL クラス) パターンの違い

一般的な DAL オブジェクトは、ストレージに対してデータ アクセスと永続化操作を直接実行します。多くの場合、1 つのテーブルと行のレベルで実行されます。 DAL クラスのセットを使用して実装される単純な CRUD 操作では、トランザクションは頻繁にサポートされません (ただし、必ずしもそうであるとは限りません)。 ほとんどの DAL クラス アプローチでは抽象化を最小限に抑え、DAL オブジェクトを呼び出すアプリケーションまたはビジネス ロジック レイヤー (BLL) クラス間の緊密な結合を実現します。

リポジトリを使用する場合、永続化の実装の詳細はドメイン モデルからカプセル化されます。 抽象化を使用すると、デコレーターやプロキシなどのパターンを使用して動作を簡単に拡張できます。 たとえば、 キャッシュ、ログ記録、エラー処理などの横断的な問題はすべて、データ アクセス コード自体でハードコーディングされるのではなく、これらのパターンを使用して適用できます。 また、ローカル開発から共有ステージング環境、運用環境まで、さまざまな環境で使用できる複数のリポジトリ アダプターをサポートすることも簡単です。

Unit of Work の実装

作業単位とは、複数 挿入、更新、または削除操作を伴う 1 つのトランザクションを指します。 簡単に言うと、Web サイトへの登録など、特定のユーザー アクションに対して、すべての挿入、更新、削除操作が 1 つのトランザクションで処理されることを意味します。 これは、複数のデータベース操作をチャット形式で処理するよりも効率的です。

これらの複数の永続化操作は、アプリケーション レイヤーのコードがコマンドを実行すると、後で 1 つのアクションで実行されます。 メモリ内の変更を実際のデータベース ストレージに適用する決定は、通常、作業単位パターンに基づいています。 EF では、作業単位パターンは DbContext によって実装され、 SaveChangesへの呼び出しが行われるときに実行されます。

多くの場合、このパターンまたはストレージに対して操作を適用する方法により、アプリケーションのパフォーマンスが向上し、不整合の可能性が軽減されます。 また、すべての意図された操作が 1 つのトランザクションの一部としてコミットされるため、データベース テーブル内のトランザクション ブロックも削減されます。 これは、データベースに対して多数の分離された操作を実行する場合と比較して効率的です。 そのため、選択した ORM は、多数の小さな個別のトランザクション実行ではなく、同じトランザクション内で複数の更新アクションをグループ化することで、データベースに対する実行を最適化できます。

作業単位パターンは、リポジトリ パターンを使用するか使用せずに実装できます。

リポジトリを必須にしないでください

カスタム リポジトリは、前述の理由から役立ちます。これは、eShopOnContainers の注文マイクロサービスのアプローチです。 ただし、DDD 設計や一般的な .NET 開発でも実装することが重要なパターンではありません。

たとえば、Jimmy Bogard は、このガイドに直接フィードバックを提供するときに、次のように述べています。

これはおそらく私の最大のフィードバックでしょう。 私は本当にリポジトリのファンではありません。主に、基になる永続化メカニズムの重要な詳細が隠れているからです。 私もコマンドのために MediatR に行くのはそのためです。 永続化レイヤーの全機能を使用して、そのドメインの動作をすべて集約ルートにプッシュできます。 私は通常、私のリポジトリをモックしたくない - 私はまだ実際のものでその統合テストを持っている必要があります。 CQRSを行くことは、リポジトリの必要性がもうなかったことを意味しました。

リポジトリは便利かもしれませんが、集計パターンとリッチ ドメイン モデルのように DDD 設計にとって重要ではありません。 したがって、リポジトリ パターンを使用するかどうかは、あなた次第です。

その他のリソース

リポジトリ パターン

作業単位パターン