.NET の世界では、データベースにアクセスする最もよく使用される方法の 1 つは、言語構文と緊密に統合されたオブジェクト リレーショナル マッパー (ORM) である Entity Framework (EF) を使用することです。 .NET 言語にネイティブな統合言語クエリ (LINQ) を使用すると、SQL の知識がなくても、通常の .NET コレクションを操作するような感覚でデータ アクセスを行うことができます。これには長所と短所がありますが、ここではそれについて暴言を吐かないようにします。しかし、それが常に引き起こす問題の 1 つは、ソフトウェア プロジェクトの構造、抽象化のレベル、そして最終的には単体テストに関する混乱です。
この投稿では、リポジトリの抽象化が常に役立つ理由を説明します。多くの人が抽象化されたデータ アクセスの用語としてリポジトリを使用していますが、同様のものに関連するリポジトリ ソフトウェア パターンもありますが、同じものではないことに注意してください。ここでは、データ アクセスの実装詳細を抽象化した一連のインターフェイスをリポジトリと呼び、設計パターンを完全に無視します。
ご存知の方は読み飛ばしていただいても構いませんが、そもそもリポジトリという考え方に至った経緯についてお話ししておかなければなりません。
先史時代のコードは、構造も何もなく、やりたいこと、または少なくとも望んでいることを実行するために、そのまま書かれていました。自動テストはなく、手動でハッキングして機能するまでテストするだけでした。各アプリケーションは、コード構造、再利用、読みやすさよりも重要なハードウェア要件を考慮して、手元にあるもので書かれました。それが恐竜を殺したのです!本当の事実。
ゆっくりとパターンが現れ始めました。特にビジネス アプリケーションの場合、ビジネス コード、データの永続性、およびユーザー インターフェイスが明確に分離されました。これらはレイヤーと呼ばれ、すぐに異なるプロジェクトに分割されました。これは、異なる懸念事項をカバーするだけでなく、レイヤーを構築するために必要なスキルが特に異なるためでもありました。 UI デザインはコード ロジックの作業とは大きく異なり、SQL やデータを永続化するために使用される言語やシステムとも大きく異なります。
したがって、ビジネスとデータ層の間の対話は、インターフェースとモデルに抽象化することによって行われました。ビジネス クラスとして、テーブル内のエントリのリストを要求するのではなく、フィルター処理された複雑なオブジェクトのリストが必要になります。永続化されたものにアクセスし、それをビジネスにとって理解可能なものにマッピングするのはデータ層の責任です。これらの抽象化はリポジトリと呼ばれるようになりました。
データ アクセスの下位レイヤーでは、CRUD のようなパターンがすぐに引き継がれました。テーブルのような構造化永続コンテナーを定義し、レコードを作成、読み取り、更新、削除します。コードでは、この種のロジックはリスト、辞書、配列などのコレクションに抽象化されます。そのため、リポジトリはコレクションのように動作するべきであり、実際の作成、読み取り、更新、削除以外のメソッドを持たないほど汎用的なものにするべきであるという意見の流れもありました。
しかし、私は強く反対します。ビジネスからのデータ アクセスを抽象化したものであるため、データ アクセスのパターンから可能な限り遠ざけ、ビジネス要件に基づいてモデル化する必要があります。ここで、特に Entity Framework だけでなく、他の多くの ORM の考え方がリポジトリの本来の考え方と衝突し始め、最終的には EF でリポジトリを決して使用しないという要求に至り、それをアンチパターンと呼びました。
モデル間の親子関係によって多くの混乱が生じます。人が含まれる部門エンティティのようなものです。部門リポジトリは人を含むモデルを返す必要がありますか?たぶんそうではありません。では、リポジトリを部門 (人なし) と人に分けて、個別の抽象化を行ってビジネス モデルにマッピングしてはどうでしょうか?
ビジネス層をサブレイヤーに分割すると、実際には混乱が増大します。たとえば、ほとんどの人がビジネス サービスと呼んでいるものは、特定のビジネス ロジックを特定の種類のビジネス モデルにのみ適用することを抽象化したものです。アプリが人を操作するので、「人」というモデルがあるとします。人を処理するクラスは PeopleService になります。これは、PeopleRepository を介して永続層からビジネス モデルを取得しますが、データ モデルとビジネス モデルの間のマッピングや、人にのみ関連する特定の作業 (ユーザーの計算など) など、他のことも実行します。給料。ただし、ほとんどのビジネス ロジックは複数のタイプのモデルを使用するため、サービスは最終的にリポジトリ上にラッパーをマッピングすることになり、追加の責任はほとんどありません。
次に、EF を使用してデータにアクセスしていると想像してください。 SQL テーブルにマップするエンティティのコレクションを含む DbContext クラスをすでに宣言する必要があります。 LINQ を使用してそれらを繰り返し、フィルタリングし、マッピングします。これはバックグラウンドで SQL コマンドに効率的に変換され、階層的な親子構造を備えた必要なものを提供します。この変換では、特定の列挙型や奇妙なデータ構造などの内部ビジネス データ型のマッピングも処理されます。では、なぜリポジトリ、さらにはサービスも必要なのでしょうか?
抽象化の層を増やすことは無意味なオーバーヘッドのように見えるかもしれませんが、プロジェクトに対する人間の理解を深め、変更の速度と質を向上させると私は信じています。明らかにバランスがあり、すべてのソフトウェア設計パターンをどこでも使用するという明白な要件を備えてシステムが設計されているのを見てきました。抽象化は、コードの読みやすさと関心の分離を向上させる場合にのみ役立ちます。
EF が面倒になる状況の 1 つは単体テストです。 DbContext は複雑なシステムであり、多大な労力をかけて手動でモックする必要がある依存関係が多数あります。そこで Microsoft は、インメモリ データベース プロバイダーというアイデアを思いつきました。したがって、何かをテストするには、メモリ内のデータベースを使用するだけで完了します。
Microsoft のページでは、このテスト方法は現在「非推奨」とマークされていることに注意してください。また、これらの例でも、EF はリポジトリによって抽象化されていることに注意してください。
メモリ内データベースのテストは機能しますが、対処が難しいいくつかの問題が追加されます。
したがって、最終的には、「ヘルパー」メソッド内でデータベース内のすべてを設定し、この不可解で複雑なメソッドで始まるテストを作成して、最小の機能さえテストすることになります。 EF コードを含むコードは、このセットアップなしではテストできません。
リポジトリを使用する理由の 1 つは、テストの抽象化を DbContext の上に移動することです。これで、データベースはまったく必要なくなり、リポジトリのモックだけが必要になります。次に、実際のデータベースを使用して統合テストでリポジトリ自体をテストします。インメモリ データベースは実際のデータベースに非常に似ていますが、少し異なります。
もう 1 つの理由は、実生活で実際の価値があることをほとんど見たことがないと認めますが、データへのアクセス方法を変更する必要があるかもしれないということです。おそらく、NoSql または何らかのメモリ分散キャッシュ システムに変更したいと思われるでしょう。あるいは、可能性の方が高いのは、データベース構造 (おそらくモノリシック データベース) から始めて、それを異なるテーブル構造を持つ複数のデータベースにリファクタリングしたいと考えていることです。初めに言っておきますが、これはリポジトリなしでは不可能です。
Entity Framework に特有のことですが、取得するエンティティはデータベースにマップされたアクティブなレコードです。あるエンティティに変更を加え、その変更を別のエンティティに保存すると、データベース内の最初のエンティティも突然更新されてしまいます。あるいは、何かを含めていないか、コンテキストが変更されているため、含めていない可能性があります。
EF の支持者は、エンティティの追跡を非常に肯定的なものとして常に誇大宣伝します。データベースからエンティティを取得し、何らかの処理を行ってから、エンティティを更新して保存するとします。リポジトリを使用すると、データを取得してからビジネスを実行し、その後、少しの更新を実行するためにデータを再度取得します。 EF はそれをメモリに保持し、変更前に更新されていないことを認識しているため、それを 2 回読み取ることはありません。それは本当だ。これらは、データベースの変更を何らかの形で認識し、特に指示がない限り、データベースから処理するすべてのものを追跡するデータベースのメモリ キャッシュについて説明しています。データベース エントリを複雑な C# エンティティに双方向にマップし、深く埋め込まれながら変更を前後に追跡します。ビジネスコードで。個人的には、この多大な責任と懸念事項の分離の欠如は、それを使用することで得られるパフォーマンスよりもはるかに有害であると信じています。さらに、最初の努力で、ビジネス、キャッシュ、データ アクセスの間の明確な境界線を保ちながら、すべての機能をリポジトリ内で抽象化することも、場合によってはリポジトリのメモリ キャッシュの別の層でさえも抽象化することもできます。
実際、これらすべてにおける実際の困難は、別々の関心事を持つべきシステム間の境界を決定することです。たとえば、フィルタリング ロジックをデータベース内のストアド プロシージャに移動すると、パフォーマンスが大幅に向上しますが、使用されるアルゴリズムのテスト容易性と可読性が失われます。逆に、EF またはその他のメカニズムを使用してすべてのロジックをコードに移動することは、パフォーマンスが低下し、場合によっては実現不可能です。あるいは、データ エンティティがビジネス エンティティになるポイントはどこですか (部門と個人に関する上記の例を参照)?
おそらく最良の戦略は、まずこれらの境界線を定義し、次にどのテクノロジーとデザインがその境界線に適合するかを決定することです。
私は、リポジトリがその下で Entity Framework や他の ORM を使用している場合でも、サービスとリポジトリの抽象化を常に使用する必要があると考えています。それはすべて、関心の分離に帰着します。私は Entity Framework が有用なソフトウェア抽象化であるとは決して考えません。Entity Framework には非常に多くの負荷が伴うため、コード内で抽象化するにはリポジトリがよく使用されます。 EF は便利な抽象化ですが、ソフトウェアではなくデータベース アクセスに使用されます。
私のソフトウェア作成の哲学は、アプリケーション要件から始めて、それらの要件に対応するコンポーネントを作成し、インターフェースを使用して下位レベルの機能を抽象化するというものです。次に、次のレベルでこのプロセスを繰り返し、コードが読みやすいこと、および現在のレベルで使用されているコンポーネントを理解する必要がないことを常に確認します。そうでない場合は、懸念事項の分離が不十分です。したがって、ビジネス アプリケーションには特定のデータベースや ORM を使用する要件がなかったので、データ層の抽象化によってそれらに関するすべての情報が隠蔽されるはずです。
ビジネスは何を望んでいますか?フィルタリングされた人物のリスト? var people = service.GetFilteredListOfPeople(filter);それ以下でもそれ以上でもありません。そしてサービスメソッドは単に return mapPeople(repo.GetFilteredListOfPeople(mappedFilter)); を実行します。繰り返しますが、それ以下でもそれ以上でもありません。リポジトリがどのように人々を獲得するか、人々を救うか、またはその他のことを行うかは、サービスの関心事ではありません。キャッシュが必要な場合は、IPeopleRepository を実装し、IPeopleRepository に依存するキャッシュ メカニズムを実装します。マッピングが必要な場合は、正しい IMapper インターフェイスを実装してください。などなど。
この記事が冗長になりすぎていないことを願っています。これはソフトウェアの問題ではなく概念的な問題であるため、コード例は特に記載しませんでした。ここでの私の不満のほとんどは Entity Framework に向けられているかもしれませんが、これは小さなことは魔法のように助けてくれるが、重要なことは壊してしまうあらゆるシステムに当てはまります。
お役に立てば幸いです!
以上がORM を含むリポジトリを使用したコード内でのデータ アクセスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。