データ ポイント

複数モデルを対象とする EF6 Code First Migrations

Julie Lerman

コード サンプルのダウンロード

Julie LermanEntity Framework 6 では Code First Migrations に対するサポートが導入され、これまでよりも適切に複数のモデルのデータを 1 つのデータベースに格納できるようになります。しかし、このサポートはかなり限定的なもので、想像とは違うかもしれません。今回は、この機能の概要を示し、この機能が実行することとしないこと、およびこの機能の使用方法について見ていきます。

異なるモデル、異なるテーブル、異なるデータ: 同じデータベース

EF6 の移行では、互いにまったく独立している複数のモデルの移行がサポートされます。この機能には、キーを使用して一連の移行を区別する実装と、データベース スキーマを使用して 1 つのモデルのテーブルによって移行履歴をグループ化する実装という 2 つの実装があり、どちらの実装でも同じデータベース内の独立した異なるモデルを格納できます。

モデル間のデータ共有またはマルチテナント データベースは対象外

この機能のメリットは誤解されやすいので、サポートされないことを明確にしておきます。

この新しい複数モデルのサポートは、1 つのモデルを、マルチテナント データベースを含む複数のスキーマ間で複製するようには設計されていません。

この機能に多く望まれるその他のパターンとして、共通エンティティ (およびそのデータ) を複数のモデル間で共有し、そのエンティティを 1 つのデータベースにマップする機能があります。しかし、これはまったく異なる種類の問題で、Entity Framework を使用して簡単に解決できる問題ではありません。以前試したことがありますが、あきらめました。データベース間のデータ共有については、本コラムで以前取り上げました (「コンテキストが限定されるドメイン駆動設計でのデータ共有パターン (第 2 部)」https://msdn.microsoft.com/ja-jp/magazine/dn857357.aspx を参照してください)。また、TechEd Europe でも「Entity Framework Model Partitioning in Domain-Driven Design Bounded Contexts」(コンテキストが限定されるドメイン駆動設計での Entity Framework モデルのパーティション分割、英語) というセッションでプレゼンテーションを行いました。このプレゼンテーションの録画は bit.ly/1AI6xPa (英語) から入手できます。

パターン 1: ContextKey が鍵

この機能を実現するために EF6 が提供する新しいツールの 1 つが ContextKey です。ContextKey はデータベースの MigrationHistory テーブルの新しいフィールドで、すべての移行を追跡します。これは、DbMigrationsConfiguration<TContext> クラスの同名の新しいプロパティと連携します。

既定では、ContextKey は、そのコンテキストに関連付けられた DbMigrationsConfiguration の厳密に型指定された名前を継承します。一例として、Doctor 型と Episode 型と連携する DbContext クラスを次に示します。

namespace ModelOne.Context
{
  public class ModelOneContext:DbContext
  {
    public DbSet<Doctor> Doctors { get; set; }
    public DbSet<Episode> Episodes { get; set; }
  }
}

通常通り、enable-migrations コマンドの既定の動作は、[名前空間].Migrations.Configuration という名前の DbMigrationsConfiguration クラスを作成することです。

特定の移行を適用すると (つまり、Visual Studio の Package Manager Console で Update­Database を呼び出すと)、Entity Framework は移行を適用するだけでなく、__MigrationHistory テーブルに新しい行を追加します。以下は、この操作の SQL です。

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model],
  [ProductVersion])
VALUES (N'201501131737236_InitialModelOne',
  N'ModelOne.Context.Migrations.Configuration',
  [hash of the model], N'6.1.2-31219')

ContextKey フィールドに ModelOne.Context.Migrations.Configuration という値が設定されているのがわかります。この値が、DbMigrationsConfiguration<TContext> クラスの厳密に型指定された名前です。

ContextKey 名を制御する場合は、クラス コンストラクターで DbMigrationsConfiguration クラスの ContextKey プロパティを指定します。次のように、名前を ModelOne に変更します。

public Configuration()
{
  AutomaticMigrationsEnabled = false;
  ContextKey = "ModelOne";
}

移行を実行すると、Migration テーブルの ContextKey フィールドに ModelOne が使用されるようになります。しかし、その前に既定の設定で移行を実行していると、これが機能しません。EF は、テーブルなどのデータベース オブジェクトを作成した移行を含め、すべての移行を再適用しようとするため、オブジェクトが重複しエラーがスローされます。そのため、この値を変更してから移行を適用することをお勧めします。そうしない場合は、__MigrationHistory テーブルのデータを手動で更新する必要があります。

DbContext 型が、MultipleModelDb という名前を付けた接続文字列を指すようにします。コンテキストと同名の接続文字列を検索するという Code First の規則を利用するのではなく、このデータベースを対象とする 1 つの接続文字列を用意して、任意のモデルで使用できようにします。これを行うには、コンテキスト コンストラクターで、接続文字列名を受け取る DbContext オーバーロードを継承します。以下は、ModelOneContext のコンストラクターです。

public ModelOneContext()
       : base("MultipleModelDb") {
}

add-migration と update-database はどちらも接続文字列を検索できるため、正しいデータベースを移行することを保証できます。

2 つのコンテキスト、2 つの ContextKey

ContextKey のしくみを理解したら、今度は独自の ContextKey を使用する別のモデルを追加します。このモデルは別のプロジェクトにします。同じプロジェクトに複数のモデルを含める場合はパターンが少し異なります。これについては後ほど詳しく説明します。以下は、ModelTwo という新しいモデルです。

namespace ModelTwo.Context
{
  public class ModelTwoContext:DbContext
  {
    public DbSet<BBCEmployee> BbcEmployees { get; set; }
    public DbSet<HiringHistory> HiringHistories { get; set; }
  }
}

ModelTwoContext は、まったく異なるドメインのクラスで機能します。以下は、ModelTwo という ContextKey を指定する DbConfiguration クラスです。

internal sealed class Configuration : DbMigrationsConfiguration<ModelTwoContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      ContextKey = "ModelTwo";
    }

ModelTwoContext を含むプロジェクトに対して update-database を呼び出すと、同じデータベースに新しいテーブルが作成され、新しい行が __MigrationHistory テーブルに追加されます。この場合の ContextKey の値は ModelTwo です。移行で実行される SQL スニペットは以下のようになります。

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
VALUES (N'201501132001186_InitialModelTwo', N'ModelTwo', [hash of the model], N'6.1.2-31219')

ドメイン、DbContext、データベースを独自に指定したため、EF の移行は、毎回適切な ContextKey を使用して、__MigrationHistory テーブルにある関連する一連の実行済みの移行をチェックします。このようにして、データベースに加えたどの変更によってモデルに変更が加えられたかを判断できます。これにより、EF は、データベースに格納されている複数の DbContext モデルの移行を正しく管理できるようになります。ただし、2 つのモデルがマップするデータベース テーブルに関してオーバーラップがない場合しか機能しないので注意が必要です。

パターン 2: データベース スキーマによるモデルと移行の分離

移行が 1 つのデータベース内の複数のモデルと連携できるようにするもう 1 つのパターンは、データベースのスキーマを使用して、移行と関連するテーブルを分離することです。これにより、単純に独立したデータベースを移行の対象にしているように考えることができます。つまり、複数のデータベースを対象にすることによって生じるリソース オーバーヘッド (メンテナンスやコスト) を考慮する必要がなくなります。

EF6 では、単一モデルのデータベース スキーマの定義が非常に簡単になっていて、新しい DbModelBuilder.HasSchema マッピングを使用してスキーマを構成するだけです。これにより、既定のスキーマ名 (SQL Server の場合は dbo) がオーバーライドされます。

ContextKey を指定しない場合でも、既定の名前が使用されることに注意してください。そのため、HasSchema プロパティが移行に影響を与える方法をデモするために設定した ContextKey のプロパティを削除することはあまり重要ではありません。

OnModelCreating メソッドの 2 つのコンテキスト クラスそれぞれ用にスキーマを設定します。以下は、ModelTwo という名前のスキーマを保持するように指定する ModelTwoContext 用の関連コードです。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("ModelTwo");
  }

他のコンテキストは、ModelOne というスキーマ名にします。

その結果、ModelTwoContext のマップ先のデータベース オブジェクトがすべて ModelTwo スキーマに含まれるようになります。また、EF は ModelTwo のスキーマ内にこのモデル用の __MigrationHistory テーブルを配置します。

これをわかりやすい方法でデモするために、前述の例とは異なるデータベースを指して、すべての移行を適用します。HasDefaultSchema メソッドを設定することはマッピングの変更で、新しい移行を追加してその変更をデータベースに適用する必要があることに留意してください。図 1 は、分離スキーマ内の移行とデータ テーブルを示しています。

データベースのスキーマにグループ化された移行とテーブル
図 1 データベースのスキーマにグループ化された移行とテーブル

移行は個別のスキーマに委ねられるため、将来、いずれかのコンテキストの移行を操作する場合は常に、EF がこれらを個別に管理することに問題は生じません。ここでは、2 つのモデルにマップされるテーブルはオーバーラップしないという重要なパターンに従っていることに注意してください。

1 つのプロジェクトの複数のモデル

ここまでのモデルの移行を分離するために ContextKey を使用する例と、データベース スキーマを使用する例は、どちらも独自のプロジェクトにカプセル化した各モデルをセットアップしました。これは、ソリューションの設計に私が好んで使用する方法です。しかし、複数のモデルを同じプロジェクトに含めることもでき、多くの場合、これが妥当な方法です。移行の編成に ContextKey とデータベース スキーマのどちらを使用する場合でも、ほんの少しのパラメーターを NuGet コマンドに追加することでこれを達成できます。

ここまでの例とは明確に区別するために、同じクラスを使用して新しいソリューションを作成します。ドメインのクラスは個別のプロジェクトのままにしておきますが、両方のモデルを同じプロジェクトに含めます (図 2 参照)。

複数の DbContext クラスを含めた 1 つのプロジェクト
図 2 複数の DbContext クラスを含めた 1 つのプロジェクト

既定では、enable­migrations によって、ソリューションで見つかった DbContext 用に Migrations というフォルダーが作成されます。複数の DbContext が見つかると、enable-migrations は、移行を作成するために DbContext をランダムに選択するわけではなく、使用する DbContext を ContextTypeName パラメーターで指示するように求めるかなり具体的なメッセージを返します。これは非常にわかりやすいメッセージで、メッセージからコピーと貼り付けを行うだけで必要なコマンドを実行できます。以下は、今回のプロジェクトで返されるメッセージです。

PM> enable-migrations
More than one context type was found in the assembly 'ModelOne.Context'.
To enable migrations for 'ModelOne.Context.ModelOneContext', use
 Enable-Migrations -ContextTypeName ModelOne.Context.ModelOneContext.
To enable migrations for 'ModelTwo.Context.ModelTwoContext', use
 Enable-Migrations -ContextTypeName ModelTwo.Context.ModelTwoContext.

今回は ContextTypeName パラメーターだけでなく、MigrationsDirectory パラメーターも追加して、フォルダーにわかりやすい名前を付け、プロジェクトのアセットを管理しやすくします。

Enable-Migrations
-ContextTypeName ModelOne.Context.ModelOneContext
-MigrationsDirectory ModelOneMigrations

図 3 は、移行ごとに作成された Configuration クラスを含む新しいフォルダーを示しています。

移行を有効にする際に DbContext とディレクトリ名を指定した結果
図 3 移行を有効にする際に DbContext とディレクトリ名を指定した結果

enable-migrations を実行すると、指定したディレクトリを認識するコードも DbConfiguration クラスに追加されます。以下に例として、ModelOneContext の構成クラスを示します (ModelTwoContext の DbConfiguraton ファイルでは、指示通りに ModelTwoMigrations がディレクトリ名に設定されます)。

internal sealed class Configuration : DbMigrationsConfiguration<ModelOne.Context.ModelOneContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      MigrationsDirectory = @"ModelOneMigrations";
    }

これで Configuration というクラスが 2 つできてしまうため、使用する際は常に完全修飾が必要になります。そこで、最初のクラス名を ModelOneDbConfig に (以下参照)、2 つ目のクラス名を ModelTwoDbConfig に変更します。

internal sealed class ModelOneDbConfig : DbMigrationsConfiguration<ModelOneContext>
  {
    public ModelOneDbConfig()
      {
        AutomaticMigrationsEnabled = false;
        MigrationsDirectory = @"ModelOneMigrations";
      }
  }

既定の設定をオーバーライドする際に ContextKey も指定できますが、今回はそのままにしておきます。前述のように DbContext クラスで HasDefaultSchema マッピング メソッドを指定しました。そのため、移行履歴のテーブルなどのデータベース オブジェクトは、それぞれ独自のスキーマに収容されます。

ここで、両方のモデル用の移行を追加して、データベースに適用します。ここでも、使用するモデルと移行を EF に指示する必要があります。移行の構成ファイルを指すことで、EF は使用するモデルと、ファイルを格納するディレクトリを認識します。

以下は ContextOne 用の移行を追加するコマンドです (構成クラス名を変更したため、ConfigurationTypeName に完全修飾名を使用する必要はなくなっています)。

add-migration Initial
  -ConfigurationTypeName ModelOneDbConfig

この結果、移行ファイルが ModelOneMigrations ディレクトリに作成されます。ModelTwo にも同じことを行い、ModelTwoMigrations ディレクトリに移行ファイルを用意します。

ここで、これらの移行を適用します。使用する移行を EF が認識するように、ConfigurationTypeName を再度指定する必要があります。以下は ModelOne 用のコマンドです。

update-database
  -ConfigurationTypeName ModelOneDbConfig

続いて、ModelTwo 用の関連コマンドを実行します。

update-database
  -ConfigurationTypeName ModelTwoDbConfig

両コマンド実行後のデータベースは、図 1 と同じようになります。

モデルを変更し、移行を追加して適用するため、各コマンドのパラメーターとして、適切な構成クラスを忘れずに指定することだけが必要です。

ドメイン駆動設計のモデル化に有効

最近の 2 部構成のデータ ポイントのコラム「コンテキストが限定されるドメイン駆動設計でのデータ共有パターン」では、複数のドメインに含まれるデータを個別のデータベースに保存して共有する方法を取り上げました。第 1 部は https://msdn.microsoft.com/ja-jp/magazine/dn802601.aspx、第 2 部は https://msdn.microsoft.com/ja-jp/magazine/dn857357.aspx を参照してください。多くの開発者から、個別のデータベースの管理はオンプレミスでは負荷が高くなり、クラウドではコストが高くなるという指摘を受けました。今回取り上げた、1 つデータベースで複数モデルのテーブルとデータをホストする手法を利用すると、データベースの完全な分離をエミュレーションすることができます。EF6 移行のこの新しいサポートにより、このような開発者にとって優れたソリューションが提供されます。


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

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