データ ポイント

Julie Lerman

Julie LermanEntity Framework (EF) は誕生してから 8 年以上がたち、その間に何回も変更を重ねてきました。このため開発者の関心は、以前のバージョンの EF を使用するアプリケーションを新しい拡張機能でどのように強化できるかという点に向かっています。私はこれまでに、EF の更新に携わる多数のソフトウェア チームをサポートしてきました。そこから学んだ知識やガイドを、2 部構成のシリーズで紹介しています。前回は、以前の EF4 アプリケーションを例に用いて、EF6 に更新する方法を説明しました (bit.ly/1jqu5yQ)。また、大きなモデル (EDMX または Code First) を小さなエンティティ データ モデルに分割するというヒントも示しました。このように分割する理由は、(不要なことも多いリレーションシップが原因で複雑になっている) 1 つの大きなモデルをアプリケーション全体で扱うのではなく、複数の目的別モデルを扱うことをお勧めしているためです。

今月は、まず具体的な例を使用して小さなモデルへの分割をさらに詳しく説明します。続いて、分割した小さなモデルを使用して、ObjectContext API から新しい DbContext API に切り替える際に発生する問題のいくつかに取り組みます。

この記事では、読者の皆さんがソリューションをリファクタリングする際に発生する可能性がある実際の問題を示すために、典型的なデモ アプリケーションよりも少し高度な、既存のアプリケーションを使用します。このアプリケーションは、拙書『Programming Entity Framework』(O'Reilly Media、2010 年) 第 2 版に掲載している、EF4 と ObjectContext API を使用して作成した小さなサンプル アプリケーションです。アプリケーションの EDMX モデルは、Database First で生成してから EF Designer でカスタマイズしています。既定の T4 コード生成テンプレートからは、EntityObject クラスを必ず継承するエンティティ クラスが作成されますが、このソリューションでは使用を中止しました。代わりに、EF4 からサポートされるようになった単純な従来の CLR オブジェクト (POCO) を作成する T4 テンプレートを使用し、このテンプレートを少しカスタマイズしました。注意していただきたいのですが、これはクライアント側アプリケーションです。したがって、ネットワークに接続していない (状態の追跡が難しい) アプリケーションで発生する問題の一部は存在しません。それでも、今回紹介する問題の多くは、両方のシナリオに適用できます。

前回のコラムでは、EF6 と Microsoft .NET Framework 4.5 を使用するようにこのソリューションをアップグレードしましたが、コードベースは変更しませんでした。ソリューションは今でも元の T4 テンプレートを使用して、POCO クラスを生成し、永続化の管理用に ObjectContext クラスを生成しています。

アプリケーションのフロントエンドでは、UI に Windows Presentation Foundation (WPF) を使用しています。アーキテクチャは階層化されていますが、アプリケーションの設計に関する私の考え方は、このアプリケーションの作成当時から明確に進化しています。それでも、更新後のコードを見ると心が躍るような、本格的なリファクタリングは控えるつもりです。

このコラムの目標は以下のとおりです。

  • 1 つのタスクに特化した小さなモデルを抽出する。
  • 新しいスタイルの POCO クラスと、永続化管理用の DbContext クラスを出力するように、小さなモデルのコード生成を変更する。
  • DbContext に切り替えたために破損した、ObjectContext を使用している既存のコードを修正する。多くの場合は、メソッドを複製し、複製したメソッドを DbContext ロジックに変更することになります。この方法を採用すると、元のモデルやその ObjectContext でまだ使用しているその他のコードが破損しません。
  • EF5 と EF6 で導入された、より簡潔で効率的な機能にロジックを置き換える機会を探る。

Trip Maintenance (旅行の管理) モデルを作成する

前回のコラムでは、大きなモデルから小さなモデルを特定して抽出するようガイドを示しました。今回も、このガイドに従ってサンプル アプリケーションのモデルを調べました。このモデルはあまり大きくありませんが、冒険旅行あっせん会社の業務に関するさまざまなタスクを管理するためのエンティティが含まれています。モデルには、目的地、活動、宿泊施設、日付などで定義されている、旅行を管理するためのエンティティがあります。また、顧客、顧客の予約、支払い、および各種連絡先情報で定義されているエンティティもあります。図 1 の左側に、モデル全体を示します。

The Full Model on the Left, the Constrained Model on the Right, Created Using the Entities Shown in Red in the Bigger Model
図 1 モデル全体 (左側) と、モデル全体のうち赤で表示されたエンティティを使用して作成した制約付きモデル (右側)

アプリケーションのタスクの 1 つは、ユーザーが WPF フォームを使用して新しい旅行の定義や既存の旅行の管理を実行できるようにすることです (図 2 参照)。

The Windows Presentation Foundation Form for Managing Trip Details
図 2 旅行の詳細を管理する Windows Presentation Foundation フォーム

この WPF フォームは、旅行の定義 (目的地とホテル、開始日と終了日、およびさまざまな活動の選択) を扱います。したがって、これらのタスクの遂行に特化したモデルを想定できます。今回は Entity Data Model ウィザードを使用して新しいモデル (図 1 参照) を作成し、関連するテーブルのみを選択しました。このモデルでは、イベントと活動の間の多対多リレーションシップを扱う結合テーブルも認識されます。

次に、新しいモデルに含まれているこれらのエンティティに、それまで元のモデルで定義していたカスタマイズと同じカスタマイズを適用しました (コードはこのカスタマイズに依存しています)。カスタマイズの主な内容は、エンティティとそのプロパティの名前の変更でした。たとえば、Event を Trip に、Location を Destination に変更しました。

ここでは EDMX を使用しますが、4 つのエンティティのみを対象とする新しい DbContext クラスを作成して、Code First 用の新しいモデルを定義することもできます。ただしその場合は、余分なリレーションシップを含まない新しい型を定義するか、特定のリレーションシップを無視するために Fluent API マッピングを使用する必要があります。2013 年 1 月のデータ ポイントのコラム「DDD 境界コンテキストで EF モデルを縮小する」(bit.ly/1isIoGE、英語) では、Code First アプローチのガイドを紹介しています。

このコンテキストでは関係ないリレーションシップを保持する手間が省けるのは、すばらしいことです。たとえば、Reservation (予約) や Reservation に結び付いているすべてのエンティティ (Payment (支払い)、Customer (顧客) など) はモデルに含まれなくなっています。

また、Lodging (宿泊施設) と Activity (活動) については、その ID と名前が旅行の管理に必要なだけなので削除しました。残念ながら、Null 非許容のデータベース列へのマッピングに関する規則が原因で、CustomerID と DestinationID は維持する必要がありました。しかし、Destination または Lodging から Trip を参照するナビゲーション プロパティについては、必要なくなったのでモデルから削除しました。

次に、コードで生成したクラスへの対処方法を考える必要がありました。他のモデルから生成されたクラスもありますが、そのようなクラスは、このモデルでは必要ない (または存在しない) リレーションシップに関係しています。私は何年もの間ドメイン駆動設計 (DDD) を専門としてきたので、新しいモデルに特化したクラス一式を用意して、他の生成されたクラスから切り離して使用することに慣れています。新しいモデルに特化したクラスは、それぞれ別々のプロジェクトに含まれており、名前空間も異なります。各クラスは共通のデータベースにマッピングされているので、ある箇所の変更が別の箇所に反映されないことを心配する必要はありません。クラスが旅行の管理タスクに特化すると、コーディングが簡潔になる一方でソリューション内に多少の重複が発生します。この冗長性は、以前のプロジェクトで既に許容している意図的なトレードオフです。図 3 の左側は、テンプレート (BreakAway.tt) と、大きなモデルに関連付けられている生成クラスを示しています。これらのテンプレートとクラスは、BreakAwayEntities という固有のプロジェクト内に存在しています。図 3 の右側については後ほど説明します。

The Original Classes Generated from the T4 Template for the Large Model, Compared to the New Model and Its Generated Classes

図 3 大きなモデル用の T4 テンプレートから生成された元のクラスと、新しいモデルとその生成クラス

前回のコラムに基づき、新しいモデルの作成時には、元のコード生成テンプレートを使用しました。なぜなら、この段階ではアプリケーションを小さなモデルで機能させることだけに集中し、EF API の変更を同時に考える必要をなくしたかったからです。そのために、既定のテンプレート ファイル (TripMaintenance.Context.tt と TripMaintenance.tt) 内のコードを、BreakAwayEntities.tt ファイル内のコードに置き換えました。また、テンプレート コードのファイル パスを変更して、新しい EDMX ファイルを指定する必要がありました。

新しいモデルでこの簡単なアプリケーションとテストを再び実行できる (データを取得するだけでなくデータの新しいグラフを編集および挿入できる) ようになるまで参照と名前空間を変更するには、全体で約 1 時間かかりました。

DbContext API に本格的に移行する

大幅に簡略化された小さいモデルを作成したので、次は WPF コード内で該当データを扱うタスクを簡略化します。ここから、さらに難しい作業が始まります。ObjectContext の直接操作を簡略化するために記述したコードを削除できるように、ObjectContext を DbContext に置き換えます。新しいモデルに関連している部分のコードだけを操作すればよくなったのは、ありがたいことです。骨折した腕をもう一度折って正しい位置に戻すようなものです。アプリケーションの他の "骨" には手を付けないので痛くありません。

EDMX 用のコード生成テンプレートを更新する: EDMX を使用し続けるので、Trip Maintenance モデルを DbContext API に切り替えるには、新しいコード生成テンプレートを選択する必要があります。この作業に必要な EF 6.x DbContext ジェネレーターは、Visual Studio 2013 にインストール済みなので簡単にアクセスできます。まず、TripMaintenance.EDMX ファイルにアタッチされている 2 つの TT ファイルを削除します。続いて、EF Designer の [コード生成項目の追加] を使用して新しいテンプレートを選択します (コード生成テンプレートの使用になじみがない方は、MSDN ドキュメント「EF Designer のコード生成テンプレート」(bit.ly/1i7zU3Y、英語) を参照してください)。

元のテンプレートがカスタマイズ済みだったことを思い出してください。新しいテンプレートへの移植が必要な重要なカスタマイズは、仮想キーワードをナビゲーション プロパティに挿入するコードの削除です。アプリケーションのコードはこの動作に依存しているので、これで遅延読み込みが発生しなくなります。T4 テンプレートを使用していて、独自のモデル用にカスタマイズした箇所がある場合、これは非常に重要な手順なので留意してください (私が以前に MSDN 向けに作成した、EDMX T4 テンプレートの編集方法を実演しているビデオを bit.ly/1jKg4jB (英語) からご覧いただけます)。

最後に、エンティティの一部と ObjectContext 用に作成した部分クラスに対処しました。関連する部分クラスを新しいモデル プロジェクトにコピーし、新しく生成されたクラスに部分クラスが結び付いていることを確認しました。

AddObject メソッド、AttachObject メソッド、および DeleteObject メソッドを修正する: では、修正を開始しましょう。破損コードの多くは ObjectContext API に対するコーディング方法にもっぱら起因していますが、ほとんどのアプリケーションに共通するメソッドがあります。これらのメソッドは格好の修正対象なので、先に修正しましょう。

ObjectContext API には、ObjectSet からオブジェクトの追加、アタッチ、および削除を行う方法が複数あります。たとえば、ある項目 (newTrip という名前の旅行のインスタンスなど) を追加する場合は、以下のように ObjectContext を直接使用できます。

context.AddObject("Trips",newTrip)

また、以下のように ObjectSet からも追加できます。

_context.Trips.AddObject(newTrip)

ObjectContext の方法は、エンティティの所属先を特定するために文字列を必要とするので、少し不格好です。ObjectSet の方法では、所属先が定義済みなのでより簡潔になりますが、やはり AddObject、AttachObject、および DeleteObject という不格好な語句を使用しています。一方 DbContext に用意されている方法は、DbSet だけです。メソッド名は Add、Attach、および Remove に簡略化され、ObjectContext クラスのコレクション メソッド名に近づいています。以下に例を示します。

_context.Trips.Add(newTrip);

DeleteObject が Remove に変更されていることに注意してください。Remove の方がコレクションとの相性が良いメソッド名ですが、"最終的にレコードをデータベースから削除する" という目的をはっきり示していないので、混乱を招くことがあります。私が見かけた中でも複数の開発者が、Collection.Remove の結果は DbSet.Remove と同じく、対象のエンティティをデータベースから削除するよう EF に指示することだと誤解していました。結果は同じではないので、よく注意してください。

その他の問題は、元のアプリケーションでの ObjectContext の使用方法に起因しています。これらの問題と同じ問題に皆さんが直面するとは限りませんが、具体的な問題とその解決方法を知っておけば、自分のソリューションで DbContext に切り替える際に何が起きても対処できるようになります。

コンテキストのカスタム部分クラスに含まれるコードを修正する: 修正の手始めに、新しいモデルを含むプロジェクトをビルドしました。このプロジェクトが正常に動作すれば、このプロジェクトに依存するプロジェクトを修正できるようになります。最初にエラーが発生したのは、元々 ObjectContext を拡張するために作成した部分クラスでした。

部分クラス内に含まれているカスタム メソッドの 1 つは ManagedEntities です。このメソッドの役割は、コンテキストによって追跡されているエンティティを確認しやすくすることでした。ManagedEntities メソッドは独自の拡張メソッド (GetObjectStateEntries のパラメーターなしオーバーロード) に依存していました。このオーバーロードの使用が、コンパイル エラーの原因でした。

public IEnumerable<T> ManagedEntities<T>() {
  var oses = ObjectStateManager.GetObjectStateEntries();
  return oses.Where(entry => entry.Entity is T)
             .Select(entry => (T)entry.Entity);
 }

依存先の GetObjectStateEntries 拡張メソッドを修正しなくても、この拡張メソッドと ManagedEntities メソッドを削除するだけで十分です。DbContext API の DbSet クラスには、代わりに使用できる Local というメソッドがあるためです。

このリファクタリングに採用できるアプローチは 2 つあります。1 つは、新しいモデルを使用するコードのうち ManagedEntities を呼び出しているすべての箇所を探し出し、ManagedEntities を DbSet.Local メソッドに置き換える方法です。コンテキストによって追跡されているすべての Trip を反復処理するために ManagedEntities を使用しているコードの例を以下に示します。

foreach (var trip in _context.ManagedEntities<Trip>())

このコードは、以下のコードに置き換えることができます。

foreach (var trip in _context.Trips.Local.ToList())

ただし、Local メソッドは ObservableCollection を返すので、エンティティを抽出するために ToList を追加しています。

もう 1 つのアプローチとして、コード内で ManagedEntities を何回も呼び出している場合は、すべての箇所を編集しなくてもよいように ManagedEntities の背後にあるロジックを変更することもできます。ManagedEntities メソッドはジェネリックなので Trips DbSet を使用すれば済むほど簡単ではありませんが、以下のように十分に簡単に変更できます。

public IEnumerable<T> ManagedEntities<T>() where T : class  {
  return Set<T>().Local.ToList();
}

最も重要な点は、オーバーロードした GetObjectStateEntries 拡張メソッドに Trip Management のロジックが依存しなくなっていることです。元の ObjectContext で引き続き使用できるように、GetObjectStateEntries メソッドはそのまま残しておけます。

最終的に、拡張メソッドを使用して実行する必要があった処理の多くは、DbContext API を使用すると不要になりました。そのような処理は非常によく必要とされるパターンだったため、DbContext API には Local メソッドなどの簡単な手段が用意されています。

部分クラスで見つけた 2 つ目の破損メソッドは、エンティティの追跡状態を Modified に設定するために使用していたメソッドでした。このメソッドも、目的を達成するためにわかりにくい EF コードの使用を余儀なくされていた箇所でした。エラーが発生したコード行は以下のとおりです。

ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);

このメソッドは、もっと簡単な DbContext.Entry().State プロパティに置き換えることができます。このコードは DbContext を拡張する部分クラスに存在しているので、TripMaintenanceContext のインスタンスからではなく、直接 Entry メソッドにアクセスできます。修正後のコード行は以下のとおりです。

Entry(entity).State = EntityState.Modified;

部分クラス内の最後のメソッドも破損していました。このメソッドは、ObjectSet.ApplyCurrentValues メソッドを使用して、追跡されているエンティティの状態を同じ型の (追跡されていない) 別のインスタンスの値に変更します。ApplyCurrentValues メソッドは、受け取ったインスタンスの ID 値を使用し、変更追跡機能で一致するエンティティを見つけたら、受け取ったオブジェクトの値を使用してそのエンティティを更新します。

DbContext には、ApplyCurrentValues メソッドに相当するメソッドがありません。Entry().CurrentValues().Set を使用すれば DbContext でも同じように値を置換できますが、そのためには追跡されているエンティティへのアクセスを確立しておく必要があります。追跡されているエンティティを見つけるジェネリックなメソッドを構築して ApplyCurrentValues メソッドを置き換える手段に簡単なものはありません。ただし、解決の糸口がないわけではありません。DbContext から ObjectContext のロジックにアクセスする機能を利用すれば、特殊な ApplyCurrentValues メソッドを引き続き使用できます。DbContext は ObjectContext のラッパーなので、DbContext API を使用すると、IObjectContextAdapter などの特別な場合に、基盤となる ObjectContext にアクセスできます。再利用しやすいように、Core という簡単なプロパティを部分クラスに追加しました。

public ObjectContext Core {
  get {
    return (this as IObjectContextAdapter).ObjectContext;
  }}

続いて、部分クラス内の関連メソッドを変更し、Core プロパティから CreateObjectSet を呼び出して、ApplyCurrentValues を使用し続けられるようにしました。これで ObjectSet にアクセスできたので、ApplyCurrentValues を使用し続けることができます。

public void UpdateEntity<T>(T modifiedEntity) where T : class {
  var set = Core.CreateObjectSet<T>();
  set.ApplyCurrentValues(modifiedEntity);
}

この最後の変更を完了すると、モデル プロジェクトをコンパイルできるようになりました。次は、UI とモデルの間の層にある破損コードに対処します。

MergeOption.NoTracking を AsNoTracking クエリに変更し、ラムダ式も使用する: EF が結果を追跡しないように指定することは、不要な処理をなくしパフォーマンスを向上するうえで重要です。今回のアプリケーションでは、参照一覧としてのみ使用するデータを数箇所でクエリしています。以下に示す例では、UI の ListBox に表示する読み取り専用の Trips と、その Destination 情報の一覧を取得しています。以前の API では、クエリの実行前に MergeOption をクエリに設定する必要がありました。以下に、当時記述しなければならなかった見栄えの悪いコードを示します。

var query = _context.Trips.Include("Destination");
query.MergeOption = MergeOption.NoTracking;
_trips = query.OrderBy(t=>t.Destination.Name).ToList();

DbSet の場合は、AsNoTracking メソッドを使用してこの処理をもっと簡潔に実行できます。また、DbContext ではついに Include メソッドでラムダ式を使用できるようになったので、ついでに Include メソッドの文字列も削除できます。変更後のコードは次のとおりです。

 

_trips= _context.Trips.AsNoTracking().Include(t=>t.Destination)
        .OrderBy(t => t.Destination.Name)
        .ToList();

DbSet.Local が再び役立つ: コード内の多くの箇所では、エンティティの ID 値しかわからない場合にそのエンティティが追跡されているかどうかを特定する必要がありました。私はこの作業をを実行するために、図 4 に示すヘルパー メソッドを作成していました。図をご覧になれば、このコードをカプセル化しようとした理由がおわかりいただけるでしょう。このヘルパー メソッドはごみ箱行きなので、わざわざ解読する必要はありません。

図 4 新しい DbSet.Local メソッドにより不要になった IsTracked ヘルパー メソッド

public static bool IsTracked<TEntity>(this ObjectContext context,
  Expression<Func<TEntity, object>> keyProperty, int keyId)
  where TEntity : class
{
  var keyPropertyName =
    ((keyProperty.Body as UnaryExpression)
    .Operand as MemberExpression).Member.Name;
  var set = context.CreateObjectSet<TEntity>();
  var entitySetName = set.EntitySet.EntityContainer.Name + 
    "." + set.EntitySet.Name;
  var key = new EntityKey(entitySetName, keyPropertyName, keyId);
  ObjectStateEntry ose;
  if (context.ObjectStateManager.TryGetObjectStateEntry(key, out ose))
  {
    return true;
  }
  return false;
}

探している Trip の値を渡して IsTracked を呼び出していた、アプリ内のコード例を以下に示します。

_context.IsTracked<Trip>(t => t.TripID, tripId)

先ほども使用した DbSet.Local メソッドを利用すると、以下のコードに置換できました。

_context.Trips.Local.Any(d => d.TripID == tripId))

これで、IsTracked メソッドを削除できました。ここまでの作業でどれほど多くのコードを削除できたか、皆さんは覚えているでしょうか。

また、アプリケーション用に作成した AddActivity という別のメソッドでは、エンティティが既に追跡済みかどうか確認する以上の処理を行う必要がありました。具体的にはそのエンティティを取得する必要がありましたが、この処理にも Local が役立ちます。AddActivity メソッド (図 5 参照) は、ObjectContext API 用に作成しなければならなかった不格好でわかりにくいコードを使用して、特定の Trip に Activity を追加します。この処理には、Trip と Activity の間の多対多リレーションシップが必要です。追跡されている Trip に Activity インスタンスをアタッチすると EF によって Activity の追跡が開始されるので、コンテキストが重複しないようにし、アプリケーションで例外が発生しないようにする必要がありました。AddActivity メソッドでは、エンティティの ObjectStateEntry の取得を試みていました。TryGetObjectStateEntry は、同時に 2 つの機能を実行しました。1 つはエントリが見つかった場合に Boolean を返すことで、もう 1 つは見つかったエントリまたは null を返すことです。エントリが null でなかった場合はそのエンティティを Trip にアタッチし、null だった場合は AddActivity メソッドに渡したエンティティをアタッチしました。説明するだけでも手間がかかるメソッドです。

図 5 ObjectContext API を使用している元の AddActivity メソッド

public void AddActivity(Activity activity)
{
  if (_context.GetEntityState(activity) == EntityState.Detached)
  {
    ObjectStateEntry existingOse;
    if (_context.ObjectStateManager
        .TryGetObjectStateEntry(activity, out existingOse))
    {
      activity = existingOse.Entity as Activity;
    }
    else     {
      _context.Activities.Attach(activity);
     }
  }
  _currentTrip.Activities.Add(activity);
}

このロジックを効率的に実行する方法について長いこと知恵を絞りましたが、完成したコードの長さはほとんど同じでした。繰り返しますが、これは多対多リレーションシップです。このため通常以上に注意する必要があります。ただし、コードは記述しやすくて読みやすくなりました。ObjectStateManager を操作する必要はまったくありません。先ほどの Destination に使用したパターンに似たパターンを使用して更新した、メソッドの該当箇所を以下に示します。

var trackedActivity=_context.Activities.Local
    .FirstOrDefault(a => a.ActivityID == activity.ActivityID);
if (trackedActivity != null) {
  activity = trackedActivity;
}
else {
  _context.Activities.Attach(activity);
}

任務完了

この最後の修正を完了すると、すべてのテストが成功し、Trip Maintenance フォームのすべての機能を問題なく使用できるようになりました。いよいよ、EF6 の新機能を活用する機会を探る段階に到達しました。

さらに重要な点として、ObjectContext では困難だったさまざまなタスクが DbContext API では大幅に簡単になったので、アプリケーションの Trip Maintenance フォームについては今後の保守が簡略化します。この小さなモデルに注目することで、他のソリューションを ObjectContext から DbContext に移行する際に活用できるさまざまな知識を習得し、今後に十分備えることができました。

同じくらい重要な点は、コード内の更新が必要な箇所とそのままにしておく箇所を適切に選択することです。私は通常、今後も保守が必要だとわかっている機能を更新対象にしています。より高度な API と連携できるかどうか心配したくないからです。二度と変更しない予定で、EF が進化しても機能し続けるコードについては、更新前に必ずよく考えます。たとえ最終的に完治させるためだとしても、骨折した腕はひどく痛むことがあるものです。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、『Programming Entity Framework』(O'Reilly Media、2010 年)、および同書の Code First 版 (O'Reilly Media、2011 年) と DbContext 版 (O'Reilly Media、2012 年) の著者でもあります。Twitter (twitter.com/julielerman、英語) で彼女をフォローし、juliel.me/PS-Videos (英語) で Pluralsight のコースをご覧ください。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Andrew Oakley に心より感謝いたします。