データ ポイント

Entity Framework 6 での Code First の 優れた機能

Julie Lerman

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

2013 年 12 月の記事「Entity Framework 6: 上級者向けエディション」(msdn.microsoft.com/magazine/dn532202) では、Entity Framework 6 (EF6) のさまざまな新機能について説明しました。しかし、前回はすべての機能について詳しく説明することができなかったので、今月は Code First 固有の EF6 強化機能についていくつか掘り下げていきます。今回説明する機能のうち 2 つは Code First のマッピングに関連するもので、残りは Code First Migrations に関連しています。

多数の Fluent API マッピングを一度に読み込む

モデルの Fluent マッピング (クラスからデータベースへのマッピング) を指定する方法は 2 つあります。1 つは、DbContext クラスの OnModelCreating メソッドで直接指定する方法です。例を次に示します。

modelBuilder.Entity()
   .Property(c=>c.Name).IsRequired().HasMaxLength(200);
 modelBuilder.Entity()
   .Property(c => c.SerialNo).HasColumnName("SerialNumber");

多数のマッピングを指定する場合は、マッピングを型ごとに個別の EntityTypeConfiguration クラスにまとめることができます。まとめたクラスは、次のようなコードを使用してモデル ビルダーに追加します。

modelBuilder.Configurations.Add(new CasinoConfiguration());
 modelBuilder.Configurations.Add(new PokerTableConfiguration());

ただし、多数のエンティティに多数のマッピングを指定する場合は、OnModelCreating メソッドで modelBuilder.Configurations.Add メソッドを何度も繰り返し使用することになります。このような単調な処理を減らすために、単一のメソッドを使用して、特定のアセンブリからすべての EntityTypeConfiguration クラスを読み込めるようになりました。次の例では、新しい AddFromAssembly メソッドを使用して、実行中のアプリケーションの実行アセンブリに指定されている構成を読み込んでいます。

modelBuilder.Configurations
   .AddFromAssembly(Assembly.GetExecutingAssembly())

このメソッドの長所は、メソッドに読み込む構成の範囲によって制限を受けない点です。EntityTypeConfiguration カスタム クラスはプライベートとしてもマークできますが、このようなクラスを AddFromAssembly メソッドで検出できます。さらに、AddFromAssembly メソッドは EntityTypeConfiguration クラスの継承階層にも対応しています。

AddFromAssembly メソッドは、コミュニティ メンバーの Unai Zorrilla が追加した多数の機能の 1 つです。詳細については、Zorrilla のブログ記事「EF6: Setting Configurations Automatically」(EF6: 構成の自動設定、bit.ly/16OBuJ5、英語) を参照してください。また、記事をご覧になった方は、ぜひ彼に感謝のコメントをお送りください。

独自の既定のスキーマを定義する

2013 年 3 月のデータ ポイントのコラム「EF6 Alpha の操作」(msdn.microsoft.com/magazine/jj991973、英語) では、Code First のスキーマ サポートについて簡単に紹介しました。EF6 の新機能の 1 つには、OnModelCreating メソッドで構成できるマッピング、HasDefaultSchema があります。HasDefaultSchema を使用すると、EF で既定の dbo を使用する代わりに、コンテキストのマップ先となるすべてのテーブルにデータベース スキーマを指定できます。記事「Entity Framework 6: 上級者向けエディション」では、DbTransaction の説明として次のような SQL を直接実行しました。

("Update Casino.Casinos set rating= " + (int) Casino.Rating)

SQL で Casino スキーマを指定したことにお気付きの方もいらっしゃるでしょう。スキーマに Casino という名前を付けた理由は、DbContext (CasinoSlotsModel) の OnModelCreating メソッドで Casino という名前を指定していたためです。

modelBuilder.HasDefaultSchema("Casino");

前回は Code First Migrations に関する EF6 の新たなサポートについても触れましたが、Code First Migrations は内部にさまざまなスキーマが指定されているデータベースに対して機能します。この機能は EF6 Alpha 以来変更がないため、詳細については最近のコラムをご覧ください。

任意の時点からデータベースを再構築する移行スクリプト

仕様書 (および仕様書を転載している記事) に記載されている Code First Migrations の新機能の 1 つには、"アイデムポテントな移行スクリプト" があります。皆さんの中にはコンピューター サイエンスの学位を取得している方やDBA の方がいらっしゃるかもしれませんが、私はそのどちらでもないため、"アイデムポテント" の意味を調べる必要がありました。IBM エンジニアが執筆した記事を参照している Wikipedia の説明 (bit.ly/9MIrRK、英語) によると、「コンピューター サイエンス分野での "アイデムポテント" という用語は (中略) 一度実行しても複数回実行しても同じ結果を生成する操作を表すときに使用」します。

データベース分野では、"アイデムポテント" という用語は、データベースの状態に関係なくそのデータベースに常に同じ影響を及ぼす SQL スクリプトを表すときに使用します。Code First Migrations でこのようなスクリプトを使用すると、移行の実行前に、同じ移行を実行済みかどうか確認します。これは Update-Database の -Script パラメーターに固有の機能です。

EF では、特定の開始点 (移行元) から場合によっては明示的な終点 (移行先) まで、移行の全段階を実行するスクリプトの作成機能が常に提供されてきました。次の NuGet コマンドを実行するとこの動作が発生します。

Update-Database -Script
   –SourceMigration:NameOfStartMigration
   –TargetMigration:NameOfEndMigrationThatIsntLatest

新機能として、このコマンドを特定の方法で呼び出すと、いっそう洗練されたスクリプトが生成されます。

Update-Database -Script -SourceMigration $InitialDatabase

$InitialDatabase の代わりに 0 を指定してもこのスクリプトは機能するようですが、今後のバージョンでサポートされるかどうかの保証はありません。

このスクリプトを実行すると、最初の移行から最新の移行までが実行されます。そのため、構文には移行先と移行元の名前を明示的に指定していません。

ただし、このコマンドの場合は、特定の移行に対する SQL の実行前に既に適用済みの移行をチェックするロジックが EF6 によってスクリプトに追加されます。以下に、スクリプトで使用するコードの例を示します。

IF @CurrentMigration < '201310311821192_AddedSomeNewPropertyToCasino'
 BEGIN
   ALTER TABLE [Casino].[Casinos] ADD [AgainSomeNewProperty] [nvarchar](4000)
   INSERT [Casino].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
   VALUES (N'201310311821192_AddedSomeNewPropertyToCasino',
     N'CasinoModel.Migrations.Configuration', HugeBinaryValue , 
     N'6.1.0-alpha1-21011')
 END

このコードでは、AddedSomeNewPropertyToCasino スクリプトをデータベースで実行済みかどうか __MigrationHistory テーブルでチェックします。実行済みの場合、この移行の SQL は実行されません。EF6 以前のスクリプトでは、実行済みかどうかをチェックすることなく SQL を実行していました。

プロバイダーに適応する移行履歴テーブル

EF6 では、カスタマイズ可能な移行履歴テーブルと呼ばれる機能を使用して __MigrationHistory テーブルの定義をカスタマイズできます。これは、既定のデータ プロバイダーとは要件が異なるサードパーティ製のデータ プロバイダーを使用する場合に重要です。図 1は、このテーブルの既定のスキーマを示しています。

このようにテーブルの定義をカスタマイズすると役に立つ例を紹介しましょう。CodePlex で、ある開発者が寄せたコメントによると、2 つの PK の各 char 型が 1 バイトを超えているために、MigrationId と ContextKey から作成した複合キーの長さが MySQL テーブルで許可されるキーの長さ (767 バイト) を超えていることを示すエラーが発生しました (bit.ly/18rw1BX、英語)。この問題の解決策として、MySQL チームは HistoryContext を内部で使用して 2 つのキー列の長さを変更すると同時に、EF6 バージョンの ADO.NET 用 MySQL Connector (bit.ly/7uYw2a、英語) を使用しています。

図 1 に示すように、__MigrationHistory テーブルでは、先ほど HasSchema を使用するコンテキスト用に定義したスキーマと同じスキーマ (Casino) を使用しています。非データ テーブルを使用するスキーマのアクセス許可には制限が必要という規則がある場合は、このテーブルのスキーマ名の変更をお勧めします。スキーマ名を変更するには、HistoryContext を使用して、たとえば "admin" スキーマを使用するように指定します。


図 1 __MigrationHistory テーブルの既定のスキーマ

HistoryContext は DbContext から派生しているため、以前に DbContext や Code First を使用したことがある方はコードに多少見覚えがあるでしょう。図 2 は、admin スキーマを指定するために定義した HistoryContext クラスを示しています。

図 2 __MigrationHistory テーブルを再定義する HistoryContext カスタム クラス

public class CustomHistoryContext : HistoryContext
 {
   public CustomHistoryContext
    (DbConnection dbConnection, string defaultSchema)
      : base(dbConnection, defaultSchema)
   {
   }
   protected override void OnModelCreating(DbModelBuilder modelBuilder)
   {
     base.OnModelCreating(modelBuilder);
     modelBuilder.Entity()
       .ToTable("__MigrationHistory", "admin");
   }
 }

また、Property().HasColumnType、HasMaxLength、HasColumnName など、おなじみの API 呼び出しも使用できます。たとえば、ContextKey の長さを変更する必要がある場合は、次のようなコードを実行します。

modelBuilder.Entity()
   .Property(h => h.ContextKey).HasMaxLength(255);

先月の記事をお読みになっていれば、EF6 の DbConfiguration についてよくご存じでしょう。DbConfiguration を使用すると、CustomHistoryContext ファイルをモデルに通知できます。カスタム DbConfiguration のコンストラクターで、使用する HistoryContext を指定する必要があります。次のコードでは、CustomHistoryContext を使用するように SQL Server プロバイダーのコンテキストを設定しています。

SetHistoryContext(
   SqlProviderServices.ProviderInvariantName,
      (connection, defaultSchema) =>
   new CustomHistoryContext(connection,
      defaultSchema));

データベースの初期化と移行の機能では、この追加のコンテキストを参照し、コンテキストに応じて SQL を構築します。図 3のテーブルは、HistoryContext カスタム クラスを使用して、__MigrationHistory テーブルのスキーマ名を admin に変更することで作成しました (列の長さを変更するサンプル コードは省略しました)。


図 3 カスタマイズした __MigrationHistory テーブル

HistoryContext は強力な機能ですが、慎重に使用する必要があります。使用しているデータベース プロバイダーで既に HistoryContext を使用して、移行先データベースに関連する __MigrationHistory テーブルを指定している場合は考慮する必要はありませんが、そうでない場合は、この機能に関する MSDN ドキュメントを確認して、そのガイダンス (bit.ly/16eK2pD、英語) に留意することをお勧めします。

カスタム移行操作を作成する

以前に移行を使用したことがあって、しかも自動的にではなく [パッケージ マネージャー コンソール] ウィンドウから明示的に移行を作成して実行した場合は、Add-Migration コマンドによって作成された移行ファイルを調べた方もいらっしゃるでしょう。その際に、Code First Migrations では、データベース スキーマに加える各変更を指定する、System.Data.Entity.Migrations.DbMigration という厳密に型指定された APIを使用していることに気が付かれたかもしれません。

図 4に、さまざまな属性を設定する CreateTable メソッドの例を示します。

図 4 DbMigrations.CreateTable メソッド

CreateTable(
   Casino.SlotMachines",
   c => new
     {
       Id = c.Int(nullable: false, identity: true),
       SlotMachineType = c.Int(nullable: false),
       SerialNumber = c.String(maxLength: 4000),
       HotelId = c.Int(nullable: false),
       DateInService = c.DateTime(nullable: false),
       HasQuietMode = c.Boolean(nullable: false),
       LastMaintenance = c.DateTime(nullable: false),
       Casino_Id = c.Int(),
     })
   .PrimaryKey(t => t.Id)
   .ForeignKey("Casino.Casinos", t => t.Casino_Id)
   .Index(t => t.Casino_Id);

メソッドを実行すると、プロバイダーによってこれらの API 呼び出しがデータベース固有の SQL に変換されます。

DbMigrations には、テーブルやインデックスを作成するメソッド、プロパティを作成または変更するメソッド、オブジェクトを削除するメソッドなどがあります。DbMigrations は図 5の機能の一覧からわかるように非常に機能豊富な API で、SQL を直接実行するだけの機能も備えています。ただし、あらゆるニーズを満すほど機能豊富ではありません。たとえば、データベース ビューの作成、アクセス許可の指定など、さまざまな操作を実行するメソッドが含まれていません。


図 5 DbMigration のデータベース スキーマ操作

ここでも、コミュニティが救いの手を差し伸べてくれました。EF6 では、カスタム移行操作を作成する機能が追加されました。この操作は、Add-Migration によって生成される移行クラスをカスタマイズすると呼び出せます。これは、CodePlex コミュニティの開発者 Iñaki Elcoro (iceclow) のおかげです。

独自の操作を作成するには、いくつかの手順を実行する必要があります。この記事では、各手順の要点を説明します。コード全体と詳しい手順は、この記事のダウンロードに含めています。

  • 操作を定義します。ここでは CreateViewOperation を定義しました (図 6 参照)。
  • 拡張メソッドを作成し、操作を指定します。これにより DbMigration から簡単に呼び出せるようになります。
public static void CreateView(this DbMigration migration,
   string viewName, string viewqueryString)
 {
   ((IDbMigration) migration)
     .AddOperation(new CreateViewOperation(viewName,
        viewqueryString));
 }
  • SqlServerMigrationSqlGenerator カスタム クラスの Generate メソッドで、操作の SQL を定義します (図 7 参照)。
  • SqlServerMigrationSqlGenerator カスタム クラスを使用するよう、DbConfiguration クラスに指定します。
SetMigrationSqlGenerator("System.Data.SqlClient",
   () => new CustomSqlServerMigrationSqlGenerator());

図 6 データベース ビューを作成するカスタム移行操作

public class CreateViewOperation : MigrationOperation
 {
   public CreateViewOperation(string viewName, string viewQueryString)
     : base(null)
   {
     ViewName = viewName;
     ViewString = viewQueryString;
   }
   public string ViewName { get; private set; }
   public string ViewString { get; private set; }
   public override bool IsDestructiveChange
   {
     get { return false; }
   }
 }

図 7 SqlServerMigrationSqlGenerator カスタム クラス

public class CustomSqlServerMigrationSqlGenerator
   : SqlServerMigrationSqlGenerator
 {
   protected override void Generate(MigrationOperation migrationOperation)
   {
     var operation = migrationOperation as CreateViewOperation;
     if (operation != null)
     {
       using (IndentedTextWriter writer = Writer())
       {
         writer.WriteLine("CREATE VIEW {0} AS {1} ; ",
                           operation.ViewName,
                           operation.ViewString);
         Statement(writer);
       }
     }
   }
 }

以上の手順をすべて実行すると、移行ファイルの新しい操作を使用できるようになり、その操作の扱い方が Update-Database に認識されます。図 8は、実際に使用する CreateView 操作を示しています。また、この図からわかるように、ビューを削除する操作を作成する必要もあります。この削除操作は、この移行をアンワインドする必要がある場合に、Down メソッドで呼び出します。

図 8 新しい CreateView 操作の使用

public partial class AddView : DbMigration
   {
     public override void Up()
     {
       this.CreateView("dbo.CasinosWithOver100SlotMachines",
                       @"SELECT  *
                         FROM    Casino.Casinos
                         WHERE  Id IN  (SELECT   CasinoId AS Id
                         FROM     Casino.SlotMachines
                         GROUP BY CasinoId
                         HAVING COUNT(CasinoId)>=100)");
     }
     public override void Down()
     {
       this.RemoveView("dbo.CasinosWithOver100SlotMachines");
     }
   }

Update-Database を呼び出すと、データベースに新しいビューが作成されたことがわかります (図 9 参照)。


図 9 Update-Database によって新しく作成されたビュー

進化を続ける Code First

Code First の主要な機能が揃ったことで、マイクロソフトとコミュニティ開発者は、柔軟性の向上を活用した機能を使用して、EF6 をさらに強化できるようになりました。しかし、この強化は EF6 のリリースで終わりになりません。CodePlex の作業項目を 6.0.1 以降のバージョンでフィルター処理すると ( bit.ly/1dA0LZf、英語)、EF6 と Code First の今後のリリースでさらに強化が進行していることがわかります。開発状況は、項目によって異なります。もしかしたら、参加してみたくなる項目が見つかるかもしれません。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .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 のコースをご覧ください。

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