次の方法で共有


Entity Framework

n 層アプリケーションで回避すべきアンチパターン

Daniel Simmons

この記事では、次の内容について説明します。

  • n 層について理解する
  • オブジェクトを分散させてはならない
  • カスタム サービスと RESTful サービスのどちらが適しているか
  • さまざまな n 層アンチパターン
この記事では、次のテクノロジを使用しています。
Entity Framework

目次

n 層について理解する
アンチパターンその 1: 密結合
アンチパターンその 2: 静的な要件を想定している
アンチパターンその 3: 同時実行の不適切な処理
アンチパターンその 4: ステートフルなサービス
アンチパターンその 5: 3 層を装う 2 層
アンチパターンその 6: シンプルさを軽視する

Entity Framework チームの一員として、私はたびたび顧客に Entity Framework を使用するアプリケーションの構築に関する話をします。私が一番よく質問を受ける話題は、おそらく n 層アプリケーションの設計に関するものです。この記事では、皆さんがアプリケーションのこの部分で成功を収めることができるように、足場となる基本的な要素について説明します。私が一般に最も重要な問題だと考えている n 層の設計のアンチパターンについて、スペースの大部分を割いて見ていくことにします。これは考慮すべき選択肢と問題が多数存在するテーマなので、特定のアプリケーションに関する決定を下す前に全体を理解するようにしてください。今後の記事では、成功を収めるための n 層パターンや、Entity Framework に固有の主要な API と問題について検討します。また、n 層アプリケーションの開発を大幅に簡素化する、Microsoft .NET Framework 4 で実装される機能について簡単に紹介する予定です。

n 層について理解する

アンチパターンに進む前に、n 層について共通の認識を持っていただく必要があります。

まず明確にしておかなければならないのは、層 (tier) とレイヤ (layer) の違いです。適切に設計されたアプリケーションには複数のレイヤがあり、その依存関係は注意深く管理されます。これらのレイヤは単一の層に含めることも、複数の層に分けることもできます。レイヤはアプリケーションの構造の概念にすぎませんが、層はというと、物理的な分離か、少なくとも必要に応じて物理的な分離を可能にする設計のことを表します。

インプロセスで実行されないデータベースと通信するアプリケーションには複数の層がありますが、データベースとアプリケーション以外の層がない場合は n 層アプリケーションとは呼ばれません。同様に、データベースを伴う ASP.NET アプリケーションは、技術的には n 層アプリケーションと呼ばれます。データベース、Web サーバー、そしてブラウザが使用されるためです。さらに、Windows Communication Foundation (WCF) または Web サービスを採用していないアプリケーションは、n 層アプリケーションとは呼ばれません。ほとんどの用途で、Web サーバーとブラウザを 1 つのクライアント層ととらえることができるためです。n 層アプリケーションとは、少なくともデータベース層、サービスを公開する中間層、およびクライアント層から成るアプリケーションのことをいいます。

表面上はたいしたことではないように見えますが、複数の層に分かれたアプリケーションを実装するのは、実はたいへんです。想像以上に多くの落とし穴があるのです。こうした落とし穴があるからこそ、Martin Fowler はその著書『エンタープライズ アプリケーション アーキテクチャ パターン』(翔泳社、2005 年) の中で、この点について非常に強い調子で次のように述べています。

「オブジェクトを分散してはいけないのである」。

Martin はこれを "分散オブジェクト設計の第 1 法則" と呼んでいます。

他のすべての設計規則と同じように、場合によってはこの法則も無視しなければなりません。アプリケーションには、より多くのコンピューティング リソースを割り当てられるように複数の層を必要とするスケーラビリティの問題が生じることがあります。データをビジネス パートナーや顧客のアプリケーションと交換しなければならない場合もあるかもしれません。セキュリティやインフラストラクチャの制約のために、アプリケーションを複数のコンピュータに分割しなければならない場合や、アプリケーションの特定のパーツが別のパーツと直接対話できないような場合もあるでしょう。複数の層が必要になったら、必ず実装する必要があるのです。

この記事で紹介するアンチパターンはさまざまなアプリケーションやテクノロジに当てはめることができますが、Entity Framework を使用してデータを保持するカスタム WCF サービスの作成と使用に重点を置いて説明することにします。

当然のことながら、数々の n 層アンチパターンは、開発者がアプリケーションの目的に注目するのを怠った結果、生み出されたものです。n 層アーキテクチャを使用するに至ったそもそもの動機に留意しなかったり、永続性に関する重要な懸念事項を無視したりすると、問題が発生しやすくなります。次のセクションでは、いくつかの一般的な問題について見ていきます。

カスタム サービスと RESTful サービスのどちらが適しているか

REST (Representational State Transfer) は、急速に普及しつつある Web サービスの一種です。そのため、RESTful サービスとカスタム Web サービスにはどのような違いがあるのか、どちらを採用すべきだろうかと自問自答されることがあるかもしれません。2 種類のサービスの主な違いは、REST サービスはリソース中心のサービスであるのに対し、カスタム サービスは操作中心のサービスであるという点です。REST の場合は、データをリソースに分割し、各リソースに URL を付与します。そしてそれらのリソースに対して標準的な CRUD (Creation (作成)、Retrieval (取得)、Update (更新)、Deletion (削除)) 操作を実装します。カスタム サービスの場合は任意のメソッドを実装できます。つまり、リソースではなく操作に重点が置かれているのです。実装する操作は、アプリケーションの特定のニーズに合わせて調整可能です。

サービスによっては、REST モデルが最適です (通常は、リソースがはっきりしており、サービスの大部分がリソースの管理に関与するものである場合)。たとえば、Exchange Server には、電子メールと予定表のアイテムを整理するための REST API があります。同様に、インターネット上には、REST API を公開する、写真の共有用 Web サイトがあります。また、REST 操作に最適とまではいかないまでも、調整して適合させることのできるサービスもあります。たとえば、電子メールの送信は、リソースを送信トレイ フォルダに追加することで実現できます。これは一般に考えられている電子メールの送信方法ではありませんが、飛躍しすぎというほどではありません。

ただし、REST 操作が適さないケースもあります。たとえば、給与支払小切手を毎月印刷するためのワークフローを開始する目的でリソースを作成する場合などです。この場合は、目的に沿った手段を用意した方がはるかに自然です。

サービスを REST の制約に合わせて調整できる場合、そうすることで得られるメリットは数多くあります。ADO.NET Data Services と Entity Framework を組み合わせて使用すれば、RESTful サービスとそれに連動するクライアントを簡単に作成できます。サービスは特定のパターンに従う必要があるため、フレームワークを利用することで、より多くの機能を RESTful サービスに自動的に組み込むことができます。また、RESTful サービスは非常にシンプルで相互運用性が高いため、さまざまに応用可能です。RESTful サービスは、対象ユーザーが事前にわからない場合に特に適しています。さらに、REST サービスは、大量の操作に対処するために拡張することもできます。

多くのアプリケーションにとって、REST は制約が多すぎて不都合です。アプリケーションによっては、ドメインを 1 つのパターンのリソースに明確に分けられないことや、操作に複数のリソースが同時にかかわることもあります。ユーザー操作や関連のビジネス ロジックが RESTful 操作にうまく対応しないことや、REST 操作では不可能なほど厳密な制御が必要なこともあります。このような場合には、カスタム サービスを使用することをお勧めします。

REST サービスとカスタム サービスの混在したアプリケーションは、いつでも構築できます。アプリケーションを構築するときの理想的な解決策は、多くの場合、両方を混在させることです。

アンチパターンその 1: 密結合

皆さんはおそらく、密結合のデメリットについて耳にしたことがあると思われます。そのため、コンポーネントをできるだけ緩やかに結合させようと、いつもやっきになっているのではありませんか。きっとそうでしょう。

疎結合は密結合よりも難しく、パフォーマンスが低くなりがちです。生真面目に開発を始めたのに、結局、コストに見合ったメリットは得られるのかという疑問が生じることもあります。クラスのインスタンスを作成し、メソッドを直接呼び出すことができるのに、なぜインターフェイスや依存関係の注入を利用するのでしょうか。DataTable にデータを追加して受け渡しする代わりに、データベースにマップされたカスタム オブジェクトで抽象化を構築するのはなぜでしょうか。

さらに悪いことに、通常は、かなり後にならないと密結合のデメリットを実感できません。短期的にはある程度効率が良く、作業を問題なく遂行できますが、結局はアプリケーションを進化させることがほとんど不可能になります。

これまでソフトウェアの構築に携わってこられた方なら、層内のモジュールに関しては、結合のトレードオフについてよく理解しておられることでしょう。緊密に連動するモジュールがある場合は密結合が適切なこともありますが、それ以外の場合は、コンポーネントどうしを適度に切り離し、アプリケーションの変更の影響が及ばないようにする必要があります。

アプリケーションの構成要素を別々の層に分割する場合は、結合の重要性はさらに大きくなります。その理由は単純です。各層は、同じ割合で変更されるとは限りません。多数のクライアントによって使用されるサービスがあるときに、すべてのクライアントがオンデマンドでアップグレードすることを保証できない場合は、クライアントを変更しなくてもサービスを変更できるようにすることをお勧めします。そうしないと、変更のせん断率とも呼ばれる問題が発生します。アプリケーションが 2 つの方向に引っ張られて無理矢理引き裂かれるようすを想像してみてください。

ポイントは、変更率が異なるアプリケーションの構成要素や、相互に密結合されている要素を特定することです。それには、まずデータベースと中間層の境界について検討します。アプリケーションの規模が大きくなると、データベースを調整してパフォーマンスを高める必要が生じます。それまでデータベースを複数のアプリケーションで共有していた場合は、他のアプリケーションを変更することなく特定のアプリケーションの中間層を進化させなければなりません。さいわいにも、ここで Entity Framework が役に立ちます。Entity Framework のマッピング システムが中間層のコードとデータベース間の抽象化を提供するためです。中間層とクライアントの間でも、先ほどと同じ疑問について検討してみる必要があります。

このアンチパターンの実際の例のうち、特によく見られる厄介な例は、テーブル アダプタを使用してデータベースからデータを取得し、Web サービスを介してクライアントと DataSet を交換するアーキテクチャです。このアーキテクチャでは、テーブル アダプタが同じスキーマを使用して DataSet にデータを移し (つまり、データベースと中間層が密結合されている)、Web サービスが同じ DataSet をクライアントと交換します (つまり、中間層とクライアントが密結合されている)。このタイプのシステムは作成が容易です。手順をわかりやすく示してくれる Visual Studio ツールがあるためです。しかしこの方法でシステムを構築した場合、システムの特定の部分を変更すると、他のすべての部分に影響が及ぶ可能性があります。

アンチパターンその 2: 静的な要件を想定している

システムの変更について考えてみると、開発者は要件は変化しないという想定の下で設計を行うことがありますが、要件の変更が特に大きな影響を及ぼす状況が 2 つ存在します。それは、クライアントが信頼済みとして扱われている場合と、クライアントの実装に使用されるテクノロジは 1 つだけだと中間層のサービスで想定されている場合です。

信頼境界が予期せず変更されることはまれですが、データの整合性、セキュリティ、および信頼性の点から言うと、ミスの影響は甚大です。たとえば、クライアントでのみ検証を行うようにし、中間層では受信するデータを検証なしでデータベースに直接送信してもかまわないと信頼している場合、最終的に問題が生じる可能性は想像以上に高くなります。サービスの実行場所がイントラネット内に限られることがわかっていても、情報の安全性を保つためにはさらなる取り組みが不可欠です。だれかが同じサービスを使用するクライアントを別に作成したり、最初のクライアントに手を加えたりして、検証をスキップする別のコード パスからサービスを呼び出すおそれがあるからです。何が起こるか、だれにもわかりません。

さらに、サービスの実装後には、通常のコードがこれまで考えてもみなかった方法で使用される可能性がきわめて高くなります。そのため、検証やアクセス制御を複数回行うことになったとしても、中間層では必ず検証を行い、ある程度のセキュリティ保護を施すべきだと一般に考えられています。

2 つ目は、クライアントが特定のテクノロジに縛られているという問題です。こちらの方が、はるかに厄介な問題となる可能性があります。テクノロジは変化し続けます。長期間使用されるアプリケーションの場合、テクノロジの調整を余儀なくさせる事態が必ず生じますが、特にその影響を受けやすいのがクライアントです。当初、機能豊富なクライアント デスクトップ アプリケーションとして設計したアプリケーションを、後から携帯電話や Silverlight に移植する必要が生じることもあります。このような場合に、DataSet を交換するようにサービスがデザインされていると、サービスと既存のすべてのクライアントに対して大規模な手術が必要になる可能性があります。

アンチパターンその 3: 同時実行の不適切な処理

密結合は DataSet を交換するうえで不利になる場合もありますが、同時実行は DataSet でうまく対処できる複雑だが重要な領域です。残念なことに、多くの開発者は同時実行の管理という言葉の意味を正しく理解していません。さらに悪いことに、同時実行に関するミスは、アプリケーションが運用環境に移された後で明らかになることの多い問題です。こうした問題は、運が良ければ明らかなエラーとして姿を現しますが、運が悪いと、検出されずに長期にわたってデータの破損を引き起こすこともあります。

根本的には、同時実行の管理は実に単純で、2 つのクライアントが同じデータをほぼ同じタイミングで変更しようとした場合にも、データの整合性が保たれるようにする作業にすぎません。特に注意深い読者なら、これらは n 層とは関係のない状況でも生じる問題であることにお気付きでしょうが、同時実行の問題は Entity Framework の n 層設計ととりわけ関係の深い問題です。というのも、Entity Framework で n 層シナリオに対処する場合、固有の同時実行の課題が生じるためです。

ほとんどのアプリケーションに最適な同時実行の管理手法は、オプティミスティックな同時実行です。多数のクライアントがデータベースに同時にアクセスする可能性があっても、同じエンティティが競合する形で変更されることはまれです。そのため特に心配する必要はありませんが、問題が生じたかどうかを検出する手段は必要です。

検出には、総称して同時実行トークンと呼ばれるプロパティが 1 つまたは複数使用されます。エンティティのいずれかの部分が変更されると、このプロパティも変更されます。アプリケーションは、エンティティを読み取る際に同時実行トークンの値を保存します。その後、そのエンティティをデータベースに書き戻す必要が生じると、データベース内の同時実行トークンの値と、エンティティが最初に読み取られたときの値がその時点で同じかどうかをまずチェックします。値が同じであれば、更新処理が続行されます。値が異なる場合は、更新処理は停止し、例外がスローされます。

Entity Framework は、エンティティのクエリが行われたときに同時実行トークンの元の値を透過的に記録し、データベースを更新する前に競合の有無を確認することで、オプティミスティックな同時実行をサポートします。n 層アプリケーションで問題になるのは、エンティティのクエリの時点から SaveChanges が呼び出されるまでの間に単一の ObjectContext インスタンスを使用してエンティティが記録されている場合に限り、このプロセスが透過的に機能するという点です。エンティティをある層から別の層にシリアル化する場合は、単一のサービス メソッドを呼び出す間だけ中間層でコンテキストを保持するというパターンをお勧めします。その後の呼び出しで、コンテキストの新しいインスタンスをスピンして各タスクを完了するようにします (ちなみに、すべてのサービス操作に対して新しいコンテキスト インスタンスを作成することは、それ自体が重要な推奨事項です。詳細については、「アンチパターンその 4: ステートフルなサービス」を参照してください)。

Entity Framework API をこの種の切り離された操作に活用する方法を学び始めたばかりの開発者は、次のような厄介なパターンに陥りがちです (ここで言う "切り離された" とは、エンティティがクエリ後にコンテキストから切り離され、別の層に送信された後、保存時に再び結び付けられるという意味です)。

  1. エンティティのクエリを行い、クライアントにシリアル化します。この時点では、同時実行トークンの値は元の値と同じであると同時に、クライアントに送信される唯一の値です。
  2. クライアントがエンティティを受け取って変更を加え、変更済みのエンティティを中間層に戻します。
  3. クライアントにもサービスにも同時実行トークンや変更されたプロパティが明示的に記録されていないため、サービスはデータベースに対してクエリを行い、新たに作成したコンテキストに現在の状態のエンティティを読み込み、データベースから取得した現在のエンティティとクライアントから戻されたエンティティの値を比較します。
  4. サービスが SaveChanges を呼び出します。SaveChanges は変更内容を保持しつつオプティミスティックな同時実行のチェックを行います。

どこが問題なのかわかりましたか。実のところ、問題は 2 つあります。

まず、エンティティの更新を行うたびにエンティティをデータベースから 2 度読み取る必要があり (1 度目は最初のクエリ時、2 度目は更新の直前)、システムに対し余分に大きな負荷がかかるという点です。

2 つ目はさらに重大な問題です。エンティティがデータベースで変更されているかどうかをチェックするために Entity Framework が使用する "元の値" が、最初のクエリではなく 2 度目のクエリで取得されています。つまり、更新の直前に行われるクエリから値が取得されているのです。したがって、Entity Framework によって行われるオプティミスティックな同時実行のチェックで問題が発生することはまずありません。最初のクエリと 2 度目のクエリの間に他のクライアントがエンティティを変更した場合、この変更の (前ではなく) 後で取得された値が同時実行のチェックに使用されるため、システムは競合を検出できません。

オプティミスティックな同時実行のチェックで問題が検出される可能性もありますが、その場合でも 2 度目のクエリと更新処理の間に少し間があるので、やはり例外を処理するプログラムを記述する必要があります。ただし、データを破損から本当に保護したことにはなりません。

正しいパターンは、クライアントでエンティティのコピーを作成し、変更されていない元のバージョンと変更済みのバージョンの両方を返すパターン、またはクライアントが同時実行トークンを変更しないように、クライアントを記述するパターンです。(おそらくこれが最善の策ですが) 同時実行トークンを行バージョン番号にすることで、トークンがサーバー トリガによって (自動的に) 更新されるのであれば、クライアント側での変更は起こりません。プロパティの現在の値をそのまま残して、元の値のストレージとして使用することができます。

クライアントのバグにより値が誤って変更されても、見かけ上だけ成功するようなことはほとんどないため、かなり安全なアプローチだと言えます。バグのために見かけ上の失敗が発生するかもしれませんが、見かけ上の成功よりはるかにましです。

このアプローチを機能させるには、中間層がクライアントからエンティティを受信したら、それをコンテキストにアタッチし、そのプロパティを調べて、手動で変更済みとしてマークする必要があります。どちらの場合にも、アンチパターンの両方の問題を一度に解決できます。データベースに対して 2 度クエリを行う必要はありません。また、後で取得した値ではなく、最初のクエリで取得したトークンの適切な値を使用して同時実行のチェックを行うことができます。

アンチパターンその 4: ステートフルなサービス

クライアント/サーバー ソリューションの開発は比較的容易ですが、次のアンチパターンは、開発者が複数のサービス呼び出し間でコンテキストを保持することで処理を容易にしようとした場合に出現します。これは同時実行の問題を回避しているため、一見、有効な手法のように思えます。中間層でコンテキストを維持すれば、コンテキスト内に元の適切なエンティティの値を保持できます。クライアントからエンティティが返されたら、更新済みのエンティティをコンテキスト内のエンティティと比較し、必要に応じて変更を適用できます。エンティティを保存する際に適切な同時実行チェックが行われるため、余分なデータベース クエリは不要です。

表面上、このアプローチは単純に見えますが、さまざまな問題が潜んでいます。コンテキストの有効期限の管理は、瞬く間に厄介な作業となります。サービスを呼び出すクライアントが複数ある場合、クライアントごとに異なるコンテキストを保持する必要があります。そうしないと、競合のおそれがあります。また、たとえこれらの問題を解決できたとしても、結局は大きなスケーラビリティの問題が立ちはだかります。

すべてのクライアントにサーバーのリソースが固定的に割り当てられていることの弊害は、こうしたスケーラビリティの問題にとどまりません。さらに、有効期限のスキームを作成して、クライアントが開始した作業単位が完了しないような事態が発生する可能性を排除しなければなりません。また、複数の中間層サーバーを備えたファームを導入してソリューションをスケールアウトする必要があると判断した場合は、セッションのアフィニティを保持し、同じサーバー (作業単位が開始されたサーバー) にクライアントが関連付けられた状態を維持する必要があります。

これらの問題に対処するために、専門的な技術と多大な労力が費やされています。実際のところ、最善の解決策は、中間層のサービスの実装をステートレスな状態で維持することで、これらの問題をまとめて回避することです。サービスの呼び出しが行われるたびに、中間層は必要なリソースをスピンし、呼び出しを処理して、その呼び出し専用のリソースをすべて解放する必要があります。複数のサービス呼び出しをまたがる作業単位のために何らかの情報を保持しておく必要がある場合は、中間層ではなくクライアントで保持するようにしてください。そうすれば、セッションのアフィニティは不要になり、完了していない作業単位を期限切れにする必要もなくなります。また、サービスの呼び出し間で特定のクライアント向けにサーバーのリソースが使用されることもありません。

アンチパターンその 5: 3 層を装う 2 層

私が頻繁に目にするもう 1 つのアンチパターンも、このプロセスを簡素化する試みです。通常、これは「Entity Framework を使用して、複数の層の間でクエリをシリアル化してもらえませんか」といった依頼の形で姿を現します。そしてこのすぐ後には、「ついでに、別の層からの更新処理もサポートしてください」という言葉が続きます。

これらはおそらくマイクロソフトが Entity Framework に追加できる機能ですが、これまでに説明した他の問題を踏まえて少し考えてみてください。そのような機能の追加が良いアイデアかどうか、疑問を持たざるを得ないでしょう。

クライアント層で Entity Framework の ObjectContext を作成し、Entity Framework のクエリを実行してエンティティをそのコンテキストに読み込み、これらのエンティティを変更する、そして SaveChanges を使用し、中間層を介してクライアントからデータベース サーバーに更新をプッシュすることができれば (これらすべてが可能であれば)、そもそもなぜ中間層が存在するのでしょうか。なぜデータベースを直接公開しないのでしょうか。

Fowler の "分散オブジェクト設計の第 1 法則" を思い出してください。この種のアーキテクチャを使用すべきなのは、本当に必要な場合だけです。本当にこのアーキテクチャが必要なら、セキュリティを強化するか、複数のサーバーを導入してスケールアウトできるようにする必要があります。そうしないと、データベースの単純なプロキシにすぎない中間層を導入して、ここで紹介した他の問題を真に解決することはできません。企業ポリシーによる制限を回避するためにこの手法を用いることもできますが、それでは n 層アプリケーションの本質をつかんでいるとは言えません。n 層アプリケーションの構築に投資して特定のニーズを満たす、または、n 層がなくてもニーズを満たすことができるのであれば n 層をまったく使わない、このいずれかを選択することをお勧めします。

アンチパターンその 6: シンプルさを軽視する

これは、最終的な n 層アンチパターンです。これまでに説明したすべてのアンチパターンを避けるという名目で、最大限の注意を払って設計され、完全に分離された、複数の層から成る、再検証可能な最高のデザインを作成する必要があると判断するのは、実に簡単です。持てる時間のすべてを費やしてインフラストラクチャを構築してもかまいませんが、その実、ユーザーには何の価値ももたらされません。

目的をよく考え、n 層に不可欠な投資を行う必要があるかどうかを検討することが重要です。シンプルなのは良いことです。場合によっては、2 層アプリケーションが最適です。それ以上の層が必要になることもありますが、すべての要素を制御し、信頼できるほか、AJAX、Silverlight、自動で展開されるワンクリック クライアントを使用できるため、変更のせん断率を気にする必要がありません。

問題を単純化できるなら、そうしてください。必要であれば完全なソリューションを得るために最大限努力すべきですが、同様に、目的を達成するうえで有効な方法で取り組むようにしてください。

Danny Simmons は、マイクロソフトの Entity Framework チームの開発マネージャです。Entity Framework などのテーマに対する彼の考えについては、blogs.msdn.com/dsimmons/ でお読みいただけます。