しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Entity Framework 4.3 Beta 1 のコードベースマイグレーションを試してみた

前回の Entity Framework 4.3 Beta 1 の自動マイグレーションを試してみた - しばやん雑記 では自動でマイグレーションを行いましたが、今回はコードベースでマイグレーションを試してみようと思います。

基本的な流れとしては自動マイグレーションと同じで、プロジェクトを作成し、モデルクラスを追加してテーブルを作ります。

public class Product
{
    public int ProductId { get; set; }

    public string Name { get; set; }
}

モデルは前回と同じです。そして Enable-Migrations コマンドを実行してマイグレーションの設定クラスを生成します。

internal sealed class Configuration : DbMigrationsConfiguration<MvcApplication4.Models.ProductContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(MvcApplication4.Models.ProductContext context)
    {
        //  This method will be called after migrating to the latest version.

        //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
        //  to avoid creating duplicate seed data. E.g.
        //
        //    context.People.AddOrUpdate(
        //      p => p.FullName,
        //      new Person { FullName = "Andrew Peters" },
        //      new Person { FullName = "Brice Lambson" },
        //      new Person { FullName = "Rowan Miller" }
        //    );
        //
    }
}

今回は自動マイグレーションは使いませんので AutomaticMigrationsEnabled は false のままにしておきます。

ここからが前回と違います。自動でマイグレーションを行わない代わりに、どのテーブルを追加・削除したか、どのカラムを追加・削除したかを自分で実装する必要があります。

簡単な例として、これも前回と同じく Summary プロパティを Product クラスに追加してマイグレーションを行ってみます。

public class Product
{
    public int ProductId { get; set; }

    public string Name { get; set; }

    // これを追加する!
    public string Summary { get; set; }
}

Summary プロパティを追加した状態で実行すると、前回と同様にエラーが表示されます。DB とモデルの構造が異なってしまっているので当然ですね。

そこで、不足している Summary カラムを追加するために Add-Migration コマンドを実行して、マイグレーション用のクラスを作成します。

Add-Migration AddSummaryMigration

すると Migrations ディレクトリの中に入力した名前のクラスが追加されます。ファイル名の頭にタイムスタンプが付いていますが、クラス名はコマンド実行時に指定した名前になっています。

中身はスキャフォールディングである程度までは自動生成してくれるので便利ですね。実際に生成されたコードは以下の通りです。

namespace MvcApplication4.Migrations
{
    using System.Data.Entity.Migrations;
    
    public partial class AddSummaryMigration : DbMigration
    {
        public override void Up()
        {
            AddColumn("Products", "Summary", c => c.String());
        }
        
        public override void Down()
        {
            DropColumn("Products", "Summary");
        }
    }
}

特徴としては Up と Down のメソッドが用意されているので、正しく実装されている場合にはマイグレーション後に元に戻すことが可能になっています。

とりあえずこれで準備は出来ましたので実際にマイグレーションを行ってみます。マイグレーションは Update-Database コマンドを使って行います。

Update-Database -TargetMigration:AddSummaryMigration

TargetMigration の後には追加したクラス名を指定してください。これでマイグレーションが実行されますが -Verbose スイッチを付けることでどのような SQL が実行されたか確認できます。

ALTER TABLE ADD COLUMN が実行されていることがわかりますね。

そしてこの変更を元に戻したい場合には -TargetMigration に "0" を指定して実行します。

Update-Database -TargetMigration:"0"

これも -Verbose スイッチを付けて実行すると SQL が出力されます。

今度は ALTER TABLE DROP COLUMN が実行されていることが確認できました。当然ながらカラムに格納されていたデータは失われるので注意してください。

折角なのでもうちょっと試してみます。外部キーも試してみたいので、Tag テーブルを追加して Product クラスに ICollection 型のプロパティを追加しました。

public class Product
{
    public int ProductId { get; set; }

    public string Name { get; set; }

    public string Summary { get; set; }

    // これを追加!
    public virtual ICollection<Tag> Tags { get; set; }
}

// クラスを追加
public class Tag
{
    public int TagId { get; set; }

    public string Name { get; set; }
}

そして忘れずに DbContext にも DbSet 型のプロパティを追加しておきます。

public DbSet<Tag> Tags { get; set; }

この状態で Add-Migration コマンドを実行してみました。実行したコマンドと出力されたコードは以下の通りです。

Add-Migration AddTagMigration
public partial class AddTagMigration : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "Tags",
            c => new
                {
                    TagId = c.Int(nullable: false, identity: true),
                    Name = c.String(),
                    Product_ProductId = c.Int(),
                })
            .PrimaryKey(t => t.TagId)
            .ForeignKey("Products", t => t.Product_ProductId)
            .Index(t => t.Product_ProductId);
    }
        
    public override void Down()
    {
        DropIndex("Tags", new[] { "Product_ProductId" });
        DropForeignKey("Tags", "Product_ProductId", "Products");
        DropTable("Tags");
    }
}

先程の例と比較すると格段にコードが複雑になっていますが、単純に Tags テーブルを作成しているだけにすぎません。よく見ると、元々存在しなかった外部キーも自動で追加されていますね。

そして今回は一度データベースを最初の状態に戻してから、マイグレーションを行ってみます。定義されているすべてのマイグレーションを実行する場合はパラメータなしで Update-Database コマンドを実行するだけです。

実行した結果は以下の通りです。整合性を保持するためにタイムスタンプごとにマイグレーションが実行されていることが分かります。

PM> Update-Database -Verbose
Using NuGet project 'MvcApplication4'.
Using StartUp project 'MvcApplication4'.
Target database connection string: 'Data Source=.\SQLEXPRESS;Initial Catalog=MvcApplication4.Models.ProductContext;Integrated Security=True;MultipleActiveResultSets=True;Application Name=EntityFrameworkMUE'.
Applying explicit migrations: [201201171524584_AddSummaryMigration, 201201171545151_AddTagMigration].
Applying explicit migration: 201201171524584_AddSummaryMigration.
ALTER TABLE [Products] ADD [Summary] [nvarchar](max)
[Inserting migration history record]
Applying explicit migration: 201201171545151_AddTagMigration.
CREATE TABLE [Tags] (
    [TagId] [int] NOT NULL IDENTITY,
    [Name] [nvarchar](max),
    [Product_ProductId] [int],
    CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
)
CREATE INDEX [IX_Product_ProductId] ON [Tags]([Product_ProductId])
ALTER TABLE [Tags] ADD CONSTRAINT [FK_Tags_Products_Product_ProductId] FOREIGN KEY ([Product_ProductId]) REFERENCES [Products] ([ProductId])
[Inserting migration history record]

実際は自動マイグレーションだけで事足りるとは思いますが、コードベースでのマイグレーションではインデックスの指定など、これまでのコードファースト開発に比べて DB に近い部分で操作が行えるので、自動マイグレーションとの組み合わせがベストだと思います。

DbMigration 内での定義については時間があるときにまとめたいと思います。