MSDN マガジン > Home > 発行物 > 2008 > July >  データ ポイント : 階層型アーキテクチャの Entity Framework
データ ポイント
階層型アーキテクチャの Entity Framework
John Papa

このコラムは、ADO.NET Entity Framework のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

コードのダウンロード : DataPoints2008_07.exe (3,549 KB)
オンラインでのコードの参照
n 層アーキテクチャの設計者が新しいテクノロジ、パターン、または方式を評価する場合には、パズルの新しいピースをアーキテクチャにはめ込む方法を検討する必要があります。Entity Framework を使用すれば、統合の問題はなくなります。Entity Framework は n 層アーキテクチャにも単層アーキテクチャにも統合することが可能です。
今月のコラムでは、Windows® Communication Foundation (WCF) テクノロジ、Windows Presentation Foundation (WPF) テクノロジ、および Model View Presenter (MVP) パターンを使用して、n 層アーキテクチャに適した Entity Framework ソリューションを構築する方法について説明します。ここでは、論理ストア データベース、データ アクセス、ドメイン モデル、ビジネス マネージャ、サービス層、プレゼンテーション層、受動的な UI 層の各層を持つサンプル アーキテクチャを紹介し、Entity Framework を使用してこれらの層を統合する方法について説明します。この記事で使用するすべてのコード例は、MSDN® Magazine Web サイトからダウンロードできます。

階層を定義する
これから説明するアプリケーションでは、ユーザーは NorthwindEF サンプル データベースおよびビュー内の顧客を検索して表示、追加、編集、または削除することができます。コードや例の説明に入る前に、サンプルの全体的なアーキテクチャについて説明します。説明の重点はアーキテクチャ自体ではなく、Entity Framework をアーキテクチャ デザインと統合する方法にあるので、変更可能で他の方式と簡単に統合できる比較的一般的なアーキテクチャを選びました。
図 1 に、一般的な階層型アーキテクチャの概要を示します。最上位の 2 層は、UI 層とプレゼンテーション層を使用したユーザー インターフェイスの表示と遷移を処理します。UI 層の実装にはさまざまなテクノロジを使用できますが、この記事およびこの記事で紹介する例では WPF を使用します。UI 層はパッシブ ビューの MVP パターンに従います。これにより、ビュー (最上位の UI 層) はプレゼンテーション層で管理され、遷移先が指定されます。プレゼンタは、ビューにデータを供給し、下位層に保存するデータをビューから取得します。さらに、一般にビューで発生したイベントに応答します。
図 1 アーキテクチャの概要 (クリックすると拡大画像が表示されます)
この記事に示す例では、プレゼンタは WCF 経由で下位層との通信を行います。また、サービスのコントラクトをガイドとして使用し、WCF 経由でサービスを呼び出します。サービス層は、サービス コントラクト インターフェイスでサービスを公開します。サービス コントラクトによってプレゼンタはサービスの呼び出し方法を確認できます。
サービス層は、プレゼンタから情報を受信し、適切なビジネス ロジックを実行してデータの収集や変更を行うための適切なビジネス層のメソッドを呼び出します。このプロジェクトのビジネス ロジックおよび LINQ to Entities コードは、ビジネス層に属します。LINQ to Entities コードは、Entity Framework から生成されたエンティティ モデルを参照します。LINQ クエリを実行すると、LINQ クエリから概念エンティティ モデル (エンティティ データ モデル : EDM) への変換、エンティティ要素からストレージ層へのマップ、およびデータベースに対して実行する SQL クエリの生成が Entity Framework によって行われます。

モデルを構築する
ここまでは、アーキテクチャ内での各層のしくみに関するおおまかな概要を示しました。次に、Entity Framework に関連する各層の主な要素を説明します。アプリケーション用のデータベースは既に存在するので、開始点となるエンティティ モデルを NorthwindEF データベースから生成しました。
先にエンティティ モデルを構築してからエンティティをデータベースにマップすることも可能です。EDM ウィザードは基本エンティティ モデルの生成に役立ちます。生成したモデルを必要に応じて変更し、継承やエンティティ分割などのドメイン モデリングの概念を取り入れることができます。図 2 の EDM ウィザードでは、すべてのテーブルおよびストアド プロシージャが EDM へのインポート対象として選択されています。
図 2 データベースからのモデルの生成 (クリックすると拡大画像が表示されます)
EDM に関して混乱しやすい問題の 1 つに、EntitySets および EntityTypes の既定の名前付け規則があります。筆者は、ドメイン モデルのすべてのエンティティに単数形の名前を使用することを好みます。ここでは、Customer のインスタンスを作成するか、List<Order> を使用して Order インスタンスの一覧を返します。各エンティティは、エンティティを定義するプロパティを持つブループリントの単数形インスタンスです。
一方、EntitySets には複数形の名前付け規則を使用することにします。EntitySets は、多くの場合、LINQ クエリで ObjectContext に一連の Customers または Orders を参照するように指示するときに使用します。
例として、次の LINQ to Entities クエリを参照してください。
var q = from c in context.Customers
        select c;
List<Customer> customerList = q.ToList();
このクエリは、LINQ to Entities に対して、実行時に Customers EntitySet にアクセスしてすべての Customer エンティティ インスタンスを返すように指示します。2 行目でクエリを実行し、customerList というローカル変数に List<Customer> を返します。この例では、EntitySets のクエリを実行して Customer (単数形) エンティティのインスタンスを取得するということがわかりやすいように、EntitySet が複数形になっています。
この名前付け規則は絶対的なものでしょうか。もちろんそうではありません。ただ、この規則に従った方がコードは読みやすくなります。そうでなければ、EDM ウィザードから返される値の既定値を取得する場合、Customers という名前の EntitySets と Customers という名前の EntityType を取得します。この場合の LINQ to Entities クエリは次のようになります。
var q = from c in context.Customers
          select c;
  List<Customers> customerList = q.ToList();
EDM ウィザードでモデルを生成すると、EntitySet および EntityType の名前を簡単に変更できます。それには、図からエンティティを選択し、[プロパティ] ウィンドウにプロパティを表示し、目的の設定を変更します (図 3 を参照)。このアプリケーションでは、Name プロパティを設定して、すべての EntityTypes を単数形に変更しています。EntitySet の Name プロパティは既に単数形になっていたので変更していません。
図 3 EntityType の名前の変更 (クリックすると拡大画像が表示されます)

動作のしくみ
次に、アプリケーションについて説明し、このアプリケーションがビュー (NWUI プロジェクトにあります) およびプレゼンタ (NWPresentation プロジェクトにあります) から始まって上位層から下位層に移行する動作のしくみを説明します。2 つのプロジェクトは、このコラムに付属のコード サンプルに含まれています。アプリケーションは顧客検索ビューを読み込みます。このビューで、ユーザーは会社名を条件にして顧客を検索できます (図 4 を参照)。このビューの実装には WPF を使用し、ユーザーがビューを操作すると、イベントが生成されます。プレゼンタはそのイベントをリスンし、適切なアクションを実行します。
図 4 顧客の検索 (クリックすると拡大画像が表示されます)
図 4 に示すように、D で始まるすべての顧客を検索する場合、ユーザーが [Search] ボタンをクリックすると、ビューでイベントが生成されます。プレゼンタは、WCF 経由でサービス層を呼び出すことにより、このイベントをリスンして応答し、CustomerSearchView に表示する顧客エンティティの一覧を取得します。ユーザーが [Search] ボタンをクリックしたときのビューのコードは次のようになります。
private void btnSearch_Click(object sender,     RoutedEventArgs e)  {
      if (FindCustomerSearchResults != null)          FindCustomerSearchResults();
  }
このコードでは、返されたエンティティの一覧を操作していません。一覧の操作はプレゼンタに任せています。ビューは、エンティティの一覧をリスト ビュー コントロールの要素にバインドする方法を確認するために、WPF のデータ バインドを使用してエンティティのプロパティを参照します。ビューがエンティティに対して行う唯一の操作は、データ バインドを通じて実行されます。
CustomerSearchView が FindCustomerSearchResults イベントを生成すると、CustomerSearchPresenter はこのイベントをリスンし、応答として、制御を受け取って検索を実行します。次のコードは、CustomerSearchPresenter クラスで、下位層で公開される WCF サービスのプロキシとなる NWServiceClient クラスのインスタンスを作成する方法を示しています。
public void view_FindCustomerSearchResults()
{
    if (this.view.CompanyNameCriteria.Length > 0)
        using (var svc = new NWServiceClient())
        {
            IList<Customer> customerList = svc.FindCustomerList(                view.CompanyNameCriteria);
            view.CustomerSearchResultsList = customerList;
        }
}
NWPresentation プロジェクトから ServiceReference を使用して NWServiceClient を参照し、サービスの呼び出し方法と返却するデータの型をプレゼンタに認識させます。プレゼンテーション層では EDM を直接参照していません。このような直接参照は避ける必要があります。代わりに、WCF を通して公開された DataContracts から目的のエンティティ型を取得します。これにより、物理ネットワークの境界を越えて、WCF 経由でプレゼンタに Entity Framework のエンティティを渡すことができます。
取得した Customer エンティティの一覧は、ビューのパブリック プロパティに設定されます。次に、ビューのこのプロパティは List<Customer> を受け取り、それをビューの DataContext にバインドします。WPF、Silverlight®、Windows フォーム、ASP.NET のいずれでも、コードにはテクノロジ固有の要素が多く、プレゼンタがデータを処理してビューに渡すと、ビューはそのビューに固有のバインドを処理します。
この方法により、同じプレゼンタを使用して、ICustomerSearchView インターフェイスを実装する任意のビューを操作できます。このアプリケーションでは、バインドの処理は、DataContext を使用した WPF のバインド方式で行われます。
コントラクトは、サービス層で呼び出すことができるメソッドと取得されたエンティティを公開します。このアプリケーションでは、Customer および Order エンティティ型を返すメソッドのみを用意しています。そのため、コントラクトに含まれるのは、これらのエンティティ型のみです。
WCF は、必要に応じてエンティティに WCF の DataContract 属性を適用することによって、そのエンティティのシリアル化を処理します。DataContracts でエンティティを公開することにより、EDM を直接参照しなくても、エンティティを UI 層で使用できます。
.NET Framework 3.5 SP1 Beta 1 の時点では、Entity Framework はグラフの自動シリアル化をサポートしています。たとえば、親エンティティに関連付けられた子エンティティが存在する場合、その親エンティティと子エンティティはシリアル化されます。サンプル アプリケーションでは、OrderManager の FindOrderList メソッドで各 Order の Order Details を一括で読み込む LINQ to Entities クエリを使用しているので、中間層から返された各 Order エンティティに、ナビゲーション プロパティを使用してアクセス可能な 1 つの List<OrderDetail> が格納されます。
シリアル化されたエンティティはプレゼンタとサービス層との間で WCF を通して受け渡しすることができますが、ObjectContext はシリアル化されず、プレゼンタにも渡されません。そのため、エンティティを UI 層で使用することは可能ですが、ObjectContext は EDM および Entity Framework のすべてのリソースにアクセスできる下位層に残されます。
ObjectContext が残されるということは、このオブジェクトを使用して UI 層でエンティティを直接取得または変更したり、UI 層の変更履歴を管理したりはできないということです。何にしてもこれらの役割は下位層に委ねるのが最適です。ただし、エンティティを下位層に渡すとき、アプリケーションは ObjectContext と同期して、エンティティに変更を保存できるようにする必要があります。
図 4 のように、ユーザーが [Search] ボタンをクリックすると、プレゼンタはサービス層を呼び出した後、NWBusinessManagers プロジェクトにあるビジネス層を呼び出して、List<Customer> を取得します。この層の主な役割は 2 つあります。その 1 つは、EDM からのデータの取得または EDM へのデータの設定です。もう 1 つの役割は、存在する可能性があるビジネス ロジックの処理です。
CustomerManager は、ObjectContext を使用して EDM の操作を処理します。そのために、コンテキストと呼ばれるローカル フィールドを定義し、そのコンテキストのインスタンスをコンストラクタに作成します。ObjectContext は各メソッドで作成および破棄できますが、必要に応じてデータベース接続リソースをオープンまたはクローズするように最適化されています。また、クラス全体で ObjectContext にアクセスできるようにしておけば、クラス内の一連のプライベート メソッドを順次渡していく必要もなく、変更履歴を維持できます。
public CustomerManager()
{
    context = new NWEntities();
}
ただし、この種類のアプリケーションでは ObjectContext を保持するのではなく、必要に応じて作成および破棄してください。ID 解決が原因で、同じオブジェクト コンテキストを保持したままにしておくと、データは不整合で古くなり、ID 解決を行う際のパフォーマンスの低下 (追跡するデータが増えるほど深刻になります) が引き起こされ、これがマルチスレッド環境での更新の問題にもつながります。
次のコードは、ビジネス層の CustomerManager クラスの FindCustomerList メソッドを示しています。このメソッドは、コンテキストにアクセスする LINQ to Entities クエリを宣言し、指定した条件で始まる Customer エンティティの一覧を要求します。このクエリを実行すると、概念層からストレージ層へのマッピングが評価され、適切な SELECT コマンドが生成されます。
public List<Customer> FindCustomerList(string companyName)
  {
      var q = from c in context.Customers
              where c.CompanyName.StartsWith(companyName)
              select c;
      return q.ToList();
  }
SQL Server® Profiler を使用して実行すると、クエリを表示できます。

変更を保存する
ここまではアプリケーションでの単純なデータ取得方法を説明してきました。次に、データの変更を保存する方法を説明します。ユーザーが顧客を編集するとき、CustomerView ビューは適切な Customer エンティティ インスタンスにバインドされているように見えます (図 5 を参照)。CustomerView がプレゼンタに対してイベントを生成すると、プレゼンタは下位層に Customer エンティティ インスタンスを要求します。
図 5 顧客の編集 (クリックすると拡大画像が表示されます)
ユーザーが顧客を変更して保存するとき、図 6 のコードを使用してプレゼンタから下位層にエンティティが渡されます。このコードでは、ユーザーが顧客を追加または変更しているかどうかを評価し、適切なサービス層のメソッドを呼び出して、エンティティを渡しています。
public virtual void view_SaveCustomer()
{
    Customer customer = view.CurrentCustomer;
    var svc = new NWServiceClient();
    switch (view.Mode)
    {
        case ViewMode.EditMode:
            svc.UpdateCustomer(customer);
            break;
        case ViewMode.AddMode:
            svc.AddCustomer(customer);
            break;
        default:
            break;
    }
    view.CurrentCustomer = FindCustomer();

}
次に、サービス層はビジネス層に制御を渡し、ビジネス層で顧客エンティティがデータベースに保存されます。顧客エンティティは ObjectContext の一部ではなくなったので、まず、次のコードのように ObjectContext の Attach メソッドを使用して 1 つに再結合する必要があります。エンティティがコンテキストにアタッチされたら、エンティティのプロパティは変更済みとしてマークされる必要があります。そのためには、コンテキストの ObjectStateManager を使用して、各プロパティの SetModified メソッドを呼び出します。これで、エンティティの変更がコンテキストに認識され、SaveChanges メソッドを発行できるようになります。SaveChanges メソッドを発行すると、SQL UPDATE コマンドが生成され、このコマンドがデータベースに対して実行されます。
public void UpdateCustomer(Customer customer)
{
    context.Attach(customer);
    customer.SetAllModified(context);     // custom extension method
    context.SaveChanges();
}
UpdateCustomer メソッドのコードでは、SetAllModified<T> という名前を付けた拡張メソッドを使用しています。この拡張メソッドによって、エンティティのすべてのプロパティを簡単に変更済みに設定できます。SetAllModified<T> は、指定されたエンティティ T の ObjectStateEntry のインスタンスを取得した後、エンティティのすべてのプロパティ名の一覧を取得して、各プロパティに対して SetModifiedProperty を呼び出す処理を繰り返します。
public static void SetAllModified<T>(this T entity, ObjectContext  context) 
where T : IEntityWithKey
{
    var stateEntry = context.ObjectStateManager.      GetObjectStateEntry(entity.EntityKey);
    var propertyNameList = stateEntry.CurrentValues.DataRecordInfo.      FieldMetadata.Select
      (pn => pn.FieldType.Name);
    foreach (var propName in propertyNameList)
        stateEntry.SetModifiedProperty(propName);
}
エンティティを最終的に保存する別の方法として、コンテキストの Refresh メソッドを呼び出すこともできます。これにより、コンテキストに対して、エンティティのデータを取得し、プロパティの値をデータベースの値で更新するように指示します。ClientWins の RefreshMode 列挙子は、元の値をデータベースから取得した最新の値に変更し、最新のデータが優先されるようにします。
StoreWins の RefreshMode は、エンティティ キャッシュ内の元の値と現在の値を、データベースから取得した値で上書きします。ClientWins が最新データを優先する場合に適した方法であるのに対し、StoreWins は変更を取り消し、UI の表示をデータベース内の最新の値で更新する場合に適しています。
context.Refresh(RefreshMode.ClientWins, customer);  // Last in wins
Entity Framework は、更新および削除コマンドの生成時に、強制的にオプティミスティックな同時実行を行います。この処理を行うために、ConcurrencyMode 属性の値が "Fixed" に設定されているプロパティの WHERE 句に元の値を含めます。
既定では、モデルの生成時に、どのフィールドも同時実行フィールドに指定されません。そのため、ユーザーが変更を保存したとき、別のユーザーの変更が誤って上書きされる可能性があります。CustomerView が開いた状態で別のユーザーが値を変更したときに、オプティミスティックな同時実行を使用する場合は、概念モデルの EntityType の ConcurrencyMode 属性を設定してください。
EDM ファイルを編集して ConcurrencyMode を Fixed に設定すると、Entity Framework に対して、この列を Update または Delete コマンドの WHERE 句に追加するように指示することになります。一致する行が見つからない場合は OptimisticConcurrencyException が生成されます。図 7 は、ユーザーがデータベース内の顧客の地域を変更した直後に別のユーザーが同じ地域を変更しようとして、この例外が生成された場合を示しています。
図 7 OptimisticConcurrencyException (クリックすると拡大画像が表示されます)
この例外をトラップして適切な処理を行うことができます。たとえば、次に示すように例外をトラップしてログに記録し、ユーザーの変更を上書きすることができます。
catch (OptimisticConcurrencyException e){
    context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
    logger.Write(e);
    context.SaveChanges();
}

削除および追加を行う
ユーザーが顧客を削除すると、CustomerManager の DeleteCustomer メソッドが顧客エンティティを取得し、削除を適用します。
context.Attach(customer);
context.DeleteObject(customer);
context.SaveChanges();
まず、Attach メソッドを使用して Customer エンティティ インスタンスを ObjectContext と再結合します。次に ObjectContext から顧客を削除する必要があります。この方法によって、Customer エンティティ インスタンスが削除されたことが ObjectContext 内の変更履歴の記録メカニズムに認識されます。最後に、SaveChanges メソッドを呼び出すと、その時点で ObjectContext はエンティティが削除されたことを認識しており、DELETE SQL コマンドを生成して実行します。
顧客を追加すると、CustomerManager の AddCustomer メソッドで顧客エンティティが取得され、次のように挿入操作が適用されます。
context.AddToCustomers(customer);
context.SaveChanges();
このエンティティ インスタンスは新規のインスタンスなので、Customer エンティティのコンテキストに追加し、Customer エンティティの新規インスタンスとしてフラグを設定する必要があります。そのためには、AddToCustomer メソッドを使用して Customer エンティティ インスタンスを ObjectContext に関連付けます。最後に、SaveChanges メソッドを呼び出すと、ObjectContext はエンティティが追加されたこと、したがって INSERT SQL コマンドを生成して実行する必要があることを認識しています。

まとめ
この記事では、Entity Framework をアーキテクチャに統合する方法、MVP パターンなどの最新パターンを使用する方法、および一般的なアーキテクチャの問題に対処する方法について説明しました。階層型アーキテクチャにおける Entity Framework の主要な構成要素には、変更履歴の記録メカニズム、LINQ to Entities との統合、ObjectContext の接続解除および再接続機能、開発者が同時実行の問題に対処するための手段があります。

ご質問やご意見は、John (mmdata@microsoft.com) まで英語でお送りください。

John Papa (johnpapa.net) は、ASPSOFT (aspsoft.com) の上級コンサルタントです。野球を熱狂的に愛し、夏の夜を家族と共にヤンキースの応援に費やします。C# の MVP であり、INETA の講演者でもある John は、何冊かの書籍を発表しており、現在も最新作の『Data Access with Silverlight 2』を執筆中です。また、主にカンファレンス (DevConnections、VSLive など) での講演で活躍しています。


Page view tracker