web-dev-qa-db-ja.com

ドメイン層と結合するデータアクセス層

データアクセスレイヤーの実装( EF6インクルード より具体的に)がドメインレイヤーの動作にどのように影響するかについて、問題があります。

説明する理論的な例:

3層のアプリケーション、DDD:

  • ドメインレイヤー:単純なドメインモデル

    • OrderにはOrderlinesがあります
    • Orderには、パブリックプロパティ_"CanBeExtended"_があります。ビジネスロジックは
      "注文ラインが10未満の場合、注文を延長できます"
    • 注:この実装は単体テストが可能で、依存関係はありません
  • データアクセスレイヤー:このレイヤーには、データベースからOrderRepositoryオブジェクトを取得するためにEF6を使用するOrderが含まれます。

    • GetOrderByIdは、この例に関連する唯一のメソッドです
    • ドメイン層を参照します。
  • サービスレイヤー:MVCスピークに「コントローラー」が含まれ、APIを介してアクセスされます

    • データアクセス層とドメイン層の両方を参照
    • メソッド_"CanOrderBeExtended"_を公開します。入力は注文のIDであり、注文を延長できるかどうかのブール値を返します

このサービスレイヤーの実装:Orderを使用してリポジトリからGetOrderByIdオブジェクトを取得し、そのプロパティCanBeExtendedを呼び出して、その値を返します。

問題:データアクセス層の実装に応じて、ドメイン層は正しく動作しません:IncludeOrderlinesOrderGetOrderByIdの場合、CanBeExtendedの動作は異なります。

バグ:Include忘れるとOrdersはすべて拡張可能です。

可能な解決策:

  1. 遅延読み込みが考慮されますが、それは私たちのお気に入りではありません。これはEF6の技術的な詳細であり、後の段階で問題が発生する可能性があり、実際のセットアップに大きく依存します。dbサーバーのアクセス速度など...

より基本的な解決策を探しています。
ドメインとデータアクセスレイヤー間のこの依存関係を緩和するにはどうすればよいですか?
Includesを忘れないようにするにはどうすればよいですか?

  1. リポジトリで複数の特定のメソッドを作成することも検討されていますが、適切にスケーリングされていないようです。

この例では、Include(o => o.OrderLines)を含む_"GetOrderWithOrderLinesById"_のようなものを使用します。
しかし、どの方法が必要かを決定するには、ドメインレイヤーでのCanBeExtendedの実装を確認する必要があります。
さらに、その実装が変更されると、リポジトリメソッドも変更する必要がある(たとえば、Includeステートメントを追加する)か、別の新しいメソッドが必要になるため、リポジトリ(データアクセス層)とドメイン層。

ファウラーもエヴァンスも、この特定のトピックに触れているようです。

どんな考えでも歓迎します!

2
jan

まず第一に

関連 私の答え 私はあなたが読むべきだと思います。

あなたの質問は、EFのあるリポジトリの基本についてのあなたの解釈に基づいており、その解釈は完全ではありません(私のものもおそらく完全であると言っているわけではありません)。これは、明らかにしている問題につながります。

ただし、現在の方法で問題を解決するのではなく、解釈を変更する必要があります。抽象化されたバリアントを使用すると、この問題に簡単に対処できるようになるため、アーキテクチャのアプローチを再検討することを強くお勧めします。


実際の質問に答える

各レイヤーの責任を忘れないでください。

  • [〜#〜] dal [〜#〜]は、データを保存および取得します。
  • [〜#〜] bll [〜#〜]は操作を実行し、必要なビジネスルールに準拠します(および他のユーザーに準拠させます)。データストアと話す必要がない場合があります。
  • Serviceレイヤー(命名に従って)は、ユーザーがBLL操作にアクセスできるようにするパブリックインターフェイスです。

問題:データアクセスレイヤーの実装に応じて、ドメインレイヤーは正しく動作しません:IncludeOrderlinesの注文のGetOrderByIdCanBeExtendedの動作は異なります。

TL; DRDALはBLLの依存関係です。 BLLのアクションはDALから受信した応答に基づいているため、BLLはDALの応答に質問できません。

ここでの中心的な問題は、BLLに裁判の呼び出しを担当させることです(この順序を延長できますか?)が、BLLに情報の検証の自律性を与えていません判断を下す必要があります。

あなたが現在持っている方法では、BLLのバリデーターは、オーダーラインの偽のリストをそれに渡すことによってだまされる可能性があります。検証は、ビジネスルールに違反するデータの保存を防ぐ防御線として機能することになっています。ご自身が指摘したように、この防御線は簡単に回避できます。単純な真実は、あなたの防衛線は効果がないということです。

BLLが注文を延長できるかどうかを判断することが期待される場合、BLLはデータベースをチェックして、現在存在する行数をカウントできる必要があります。それがなければ、BLLはこの判断を下すことができません。

言い換えれば、あなたは以下の線に沿って何かを期待します:

public bool CanOrderBeExtended(Order order)
{
    var existingOrderLineCount = unitOfWork
                                     .OrderLineRepository
                                     .GetByOrderId(order.Id)
                                     .Count();

    return existingOrderLineCount < 10;
}

そして、そこにあなたの答えがあります。外部のコンシューマがBLLに渡すデータに基づいて、BLLは異なる動作をしません。 BLLは、データストア自体を確認することでデータストアを保護します。

OrderオブジェクトがBLL自体によってフェッチされ、注文をフェッチするために使用されたDALメソッドは、既存のすべての注文ラインもフェッチされることをすでに保証していることに注意してください(例:GetOrderWithAllOfItsOrderLines);この場合、DAL自体が現在のオーダーラインのリストが正しいことをすでに保証しているため、検証でデータベースに再度アクセスする必要はありません。

このようなものも正しいでしょう:

public OrderDto GetOrderById(int orderId)
{
    var order = unitOfWork
                    .OrderRepository
                    .GetOrderWithAllOfItsOrderLines(orderId);

    var result = new OrderDto()
                 {
                     Order = order
                     CanBeExtended = order.OrderLines.Count() < 10;
                 };

    return result;
}

BLLはDALに依存して、データストアの実際のコンテンツを提供します。

しかし、データストアがすべてのオーダーラインを返さない場合はどうなりますか?

データストアがそのジョブを実行していない場合(たとえば、この注文のデータベースに15の注文ラインがある場合でも、1つの注文ラインのみが返されます)、BLLはそれに対する保護を期待できません。これは、(テストを介して)検出し、DALで解決する必要がある問題です。

DALはBLLの依存関係です。 BLLはDALに依存しています。 DALが正しくない場合、BLLはそれ自体の依存関係が何を伝えているかを知ることが期待できません。


提案するソリューション

  1. 遅延読み込みが考慮されますが、それは私たちのお気に入りではありません。これはEF6の技術的な詳細であり、後の段階で問題が発生する可能性があり、実際のセットアップに大きく依存します。dbサーバーのアクセス速度など...

BLLから遅延ロードする場合は、IQueryableがリークしていることを意味します。 これを行わないでください。 DALの外部でIQueryableを漏らしているということは、レイヤーの分離を危うくしているということです。

強調している問題(2番目のDB呼び出し)は問題をさらに悪化させますが、これを行うべきではない理由に関する主な決定点ではありません。 2番目のdb呼び出しを使用する有効なソリューションがあります。これは、この特定のインスタンスでは必要ないかもしれませんが、それでも遅延ロードよりも優れたソリューションです。

  1. リポジトリで複数の特定のメソッドを作成することも検討されていますが、適切にスケーリングされていないようです

それはあなたがそれを合理的に期待することができるのと同様にスケーリングします。

このコンテキストでは、「うまくスケーリングしない」と述べていることは、現在処理しているエンティティの関連エンティティをロードする一般的なソリューションを期待していることを意味しているようです。それはDALの内部責任を危うくするので、それは合理的な期待ではありません。

DALは、データベースからフェッチするデータを決定します。 BLLはそれを決定しません。よくても、BLLは、DALexpliticlyが公開するメソッドを使用してDALと通信することが許可されています。
簡単に言えば、DALは、コンシューマが注文明細を取得するよう要求することを許可することを決定し(= DALはパブリックメソッドを公開します)、コンシューマ(この場合はBLL)はこのパブリックメソッドを使用してDALが許可したことを実行します(注文明細のフェッチ)。

明示的に定義されたDALメソッドなしでBLLがフェッチされたデータを動的に変更できるようにすることは、IQueryableをリークしていることを意味します(前述のように、これを行わないでください ))、またはDALには、この目的のために作成した特定のpublciメソッドがあります(IQueryableリークを回避します)。

後者は、本質的に、BLLが呼び出すDALメソッドをカスタムビルドしたことを意味します。したがって、排除することで、「適切にスケーリング」するメソッドを作成できる唯一の方法は、IQueryableをリークすることですが、これは行うべきではありません。

要するに、何かがどれほどスケーラブルである必要があるかについてのあなたの期待は、レイヤーの分離を危うくすることになります。レイヤーの分離を維持する場合、DALは、関連エンティティの取得を可能にするカスタムメソッド(つまり、EFライブラリから渡されるだけでなく、DAL開発者によって作成されたもの)を明示的に公開する必要があります。

ただし、どのメソッドが必要かを決定するには、ドメインレイヤーでのCanBeExtendedの実装を確認する必要があります。

これは、さまざまな混乱を招くポイントです。

カートを馬の前に置いていると思います。 BLLがデータを検証したいという理由だけで、DALには注文明細は含まれません。 DALには、注文ラインが含まれます。これは、DALが個人的に決定したか、明示的に含めるように要求されるためです

DALがそれらを含めるように求められた理由(BLLによって)は、DALが気にする限り、無関係です。はい、BLLはDALにビジネスロジックを検証するための注文ラインを具体的に要求した可能性がありますが、DALはそれを知る必要はありません。知っておく必要があるのは、データストアから注文明細をフェッチするように要求されたため、データストアから注文明細を返すことだけです。

それがDALの唯一の責任です。 DALはビジネスロジックよりも1層深く住んでいるため、ビジネスロジックはまったく気になりません。

4
Flater

あなたの建築は健全です。

ここでの問題の根本は...まあ...実際にはdefinedドメインモデルではないということです。多くの点で、質問は「開発者が正しいコードを確実に作成できるようにするにはどうすればよいですか?」のように言い換えることができます。ええと...できません。

ここで私が目にしているのは、dataモデル(EF)をdomainモデルとして使用することによって作成された緊張です。これら2つのモデルの不一致が発生しました。明らかにときどきのみがクエリ時に正しいbusiness回答を返すプロパティ(CanBeExtended)を含むドメインエンティティを持つことはできません。これについて考える。 「注文を延長できる(trueを返すのは注文明細が10未満の場合のみ)」というビジネスルールがあり、しないが常にその不変式を適用することをモデル化しています。

OrderLineがハイドレートされるたびにOrderコレクション全体を含めるか、追加の集計データを含める必要があります(例:int TotalNumberOfOrderLines)これは、現在コレクション内にある数とは無関係に不変式を強制するために使用できます(このオプションもよりパフォーマンスが高くなります)。それは本当にそれと同じくらい簡単です。

これを実行するためにEFを利用できるかどうかに応じて、アドホッククエリを停止し、Orderの取得を明示的にカプセル化する必要がありますdomainRepositoryは、ハイドレーションがルールを適用するのに十分なことを確認するためです。この時点で、データモデルとドメインモデルの分離の最初のステップがわかります。

重要なことに、これは@Flaterとの会話に関連しており、ドメインモデルはRepositoryインスタンスを直接参照する必要はありません。これはテストを必要以上に難しくする(そして依存関係を作成する)だけでなく、alwaysServiceでユースケースを実行するために必要なデータを知ることが可能です。レベル:

//サービスフローの概要

  1. ユースケースを実施するために必要なデータ(モデル)を取得
  2. 目的を達成するための調整データ(モデル)
  3. 永続データ(モデル)

これにより、ステップ1でステップ2に必要なデータがわかるため、パフォーマンスが向上し、ステップ2の準備としてトップレベルのクエリ(最適化可能)を発行できるようになります。

1
king-side-slide

これは単なるバグであり、今後の修正方法と回避方法を確認する必要があります。しかし、コンパイラの時間結合の問題があるかどうかを見てみましょう

@flaterの答えにほぼ同意します。別の言語で開始して、別のビューを提供しています

設計では、注文と注文明細はエンティティです。注文も集計です。したがって、ドメイン主導の設計用語では、それが意味することは

  • リポジトリはIDで注文ラインを取得できません
  • 注文明細を変更する場合は常に、関連する注文をリポジトリーから取得し、それに関連付けられている注文明細を変更して、注文を保存する必要があります。
  • リポジトリには、注文を取得して注文を保存するメソッドが必要です
  • 注文が取得されると、リポジトリは完全な注文を取得します。

これで、注文を作成するリポジトリができました。ただし、リポジトリでInclude(o => o.OrderLines)のコーディングに失敗した場合(またはOrderLinesに遅延読み込みを使用しない場合)、正しくない集計を作成するリポジトリがあります。 (OrderLineがあったときに、OrderLineのないOrderを作成しました)

今私はあなたの質問をこのように解釈します:「バグのないリポジトリをコーディングし、バグが誤って導入されないようにする方法」。私の提案は、リポジトリを単体テストすることです。使用しているデータベース(SqlSever、PGSql)に応じて、モックデータベースサーバーを作成するためにnugetパッケージを使用できます。例 https://www.nuget.org/packages/Postgres2Go/

考慮すべきもう1つのポイントはドメインはコンパイル時にリポジトリに依存するべきではありませんが、ドメインは実行時にリポジトリに依存することになります。そのため、リポジトリにバグがある場合、ドメインオブジェクトがあなたの場合のように正しく生成されない可能性があります。