在 Entity Framework Code First 应用程序中对抗迁移膨胀

Combating Migration Bloat in Entity Framework Code First Applications

既然我们中的一些人拥有 Code First 项目,这些项目已经 运行 投入生产多年,并且积累了大量的迁移,有没有人 运行 遇到过大量迁移的问题?有没有"too many migrations?"

如果是这样,补救措施是什么?一些警告: - 显然我们不能删除和重新构建生产数据库。 - 我们不能删除所有迁移,__MigrationHistory,并创建一个新的初始(在我的例子中),因为我们的许多迁移都有数据 seeds/updates,甚至对生成的命令进行调整。

是否有 way/tool 将迁移合并为更少的迁移?那会有什么不同吗?

谢谢!

根据 ISHIDA 的建议,我创建了一个组合迁移方法的示例。这绝不是 only/correct 解决方案,也没有回答迁移膨胀是否是一个问题的问题,但这是一个很好的开始。

为了对此进行测试,我有一个包含 2 table 的控制台应用程序,它们是:

public class Account
{
    [Required]
    [StringLength(100)]
    public string Id { get; set; }

    [Required]
    [StringLength(10)]
    public string AccountNumber { get; set; }

    public virtual List<Policy> Policies { get; set; }
}

public class Policy
{
    [Required]
    [StringLength(100)]
    public string Id { get; set; }

    [Required]
    public int PolicyNumber { get; set; }

    [Required]
    public string AccountId { get; set; }
    public virtual Account Account { get; set; }
}

有 4 个迁移创建了这些 table,添加了数据,并将 PolicyNumber 的数据类型从字符串更改为整数。假设这个程序是实时的并且所有这些都在生产环境中 运行。

public partial class InitialCreate : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Accounts",
            c => new
                {
                    Id = c.String(nullable: false, maxLength: 100),
                    AccountNumber = c.String(nullable: false, maxLength: 10),
                })
            .PrimaryKey(t => t.Id);
    }

    public override void Down()
    {
        DropTable("dbo.Accounts");
    }
}

public partial class SeedAccounts : DbMigration
{
    readonly string[] accountIds = new string[] { "IdAcct101", "IdAcct102" };

    public override void Up()
    {
        Sql($"INSERT INTO Accounts (Id, AccountNumber) VALUES ('{accountIds[0]}','101')");
        Sql($"INSERT INTO Accounts (Id, AccountNumber) VALUES ('{accountIds[1]}','102')");
    }

    public override void Down()
    {
        Sql($"DELETE FROM Accounts WHERE ID = '{accountIds[0]}'");
        Sql($"DELETE FROM Accounts WHERE ID = '{accountIds[1]}'");
    }
}
}

public partial class AddPolicyTable : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Policies",
            c => new
                {
                    Id = c.String(nullable: false, maxLength: 100),
                    PolicyNumber = c.String(nullable: false, maxLength: 100),
                    AccountId = c.String(nullable: false, maxLength: 100),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Accounts", t => t.AccountId, cascadeDelete: true)
            .Index(t => t.AccountId);

    }

    public override void Down()
    {
        DropForeignKey("dbo.Policies", "AccountId", "dbo.Accounts");
        DropIndex("dbo.Policies", new[] { "AccountId" });
        DropTable("dbo.Policies");
    }
}

public partial class ChangeAndSeedPolicies : DbMigration
{
    readonly string[] accountIds = new string[] { "IdAcct101", "IdAcct102" };
    readonly string[] policyIds = new string[] { "IdPol101a", "IdPol101b", "IdPol102a" };

    public override void Up()
    {
        AlterColumn("dbo.Policies", "PolicyNumber", c => c.Int(nullable: false));

        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[0]}', '{accountIds[0]}', '10101')");
        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[1]}', '{accountIds[0]}', '10102')");
        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[2]}', '{accountIds[1]}', '10201')");

    }

    public override void Down()
    {
        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[0]}'");
        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[1]}'");
        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[2]}'");

        AlterColumn("dbo.Policies", "PolicyNumber", c => c.String(nullable: false, maxLength: 100));
    }
}

这是项目 Main 中的代码:

        using (var dc = new DataContext())
        {
            foreach (var account in dc.Accounts.OrderBy(q => q.AccountNumber).ToList())
            {
                Console.WriteLine("Account " + account.AccountNumber);

                foreach (var policy in account.Policies)
                    Console.WriteLine("    Policy " + policy.PolicyNumber);
            } 
        }

数据上下文Class:

public class DataContext : DbContext
{
    public DataContext() : base("DefaultConnection") { }

    public DbSet<Account> Accounts { get; set; }
    public DbSet<Policy> Policies { get; set; }
}

输出为:

Account 101
    Policy 10101
    Policy 10102
Account 102
    Policy 10201

很简单。现在我想将这些迁移合并为一个。记住:

  • 我们不想放弃和重新搭建,因为生产会有数据 除了迁移添加的内容
  • 必须能够重新运行 之前的迁移以用于集成测试和新环境

这些是我遵循的步骤:

  • 备份您将运行针对的任何环境。
  • 创建一个新的迁移(它应该是空白的,因为还没有任何改变)
  • 在包控制台管理器 (PMC) 中,运行 "update-database" 创建 __MigrationHistory 记录
  • 此时验证应用运行正常
  • 将旧迁移中的所有 Up 方法复制到新迁移
  • 将所有 Down 方法从旧迁移复制到新迁移以相反的顺序
  • 此时正常验证应用 运行(应检测到不需要新的迁移)
  • 删除所有旧迁移
  • 删除所有旧的__MigrationHistory记录(只留下新的)
  • 此时验证应用运行正常

要验证新迁移是否确实完成了旧迁移所做的一切(对于新环境或测试),只需删除数据库中的所有 table(包括 __MigrationHistory),运行 "update-database" 在 PMC 中,看看是否 运行s.

这是我的新迁移的样子:

public partial class CombinedMigration : DbMigration
{
    readonly string[] accountIds = new string[] { "IdAcct101", "IdAcct102" };
    readonly string[] policyIds = new string[] { "IdPol101a", "IdPol101b", "IdPol102a" };

    public override void Up()
    {
        CreateTable(
            "dbo.Accounts",
            c => new
            {
                Id = c.String(nullable: false, maxLength: 100),
                AccountNumber = c.String(nullable: false, maxLength: 10),
            })
            .PrimaryKey(t => t.Id);

        Sql($"INSERT INTO Accounts (Id, AccountNumber) VALUES ('{accountIds[0]}','101')");
        Sql($"INSERT INTO Accounts (Id, AccountNumber) VALUES ('{accountIds[1]}','102')");

        CreateTable(
            "dbo.Policies",
            c => new
            {
                Id = c.String(nullable: false, maxLength: 100),
                PolicyNumber = c.String(nullable: false, maxLength: 100),
                AccountId = c.String(nullable: false, maxLength: 100),
            })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Accounts", t => t.AccountId, cascadeDelete: true)
            .Index(t => t.AccountId);

        AlterColumn("dbo.Policies", "PolicyNumber", c => c.Int(nullable: false));

        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[0]}', '{accountIds[0]}', '10101')");
        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[1]}', '{accountIds[0]}', '10102')");
        Sql($"INSERT INTO Policies (Id, AccountId, PolicyNumber) VALUES ('{policyIds[2]}', '{accountIds[1]}', '10201')");
    }

    public override void Down()
    {
        // Each prior "Down" section was added in reverse order.

        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[0]}'");
        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[1]}'");
        Sql($"DELETE FROM Policies WHERE ID = '{policyIds[2]}'");

        AlterColumn("dbo.Policies", "PolicyNumber", c => c.String(nullable: false, maxLength: 100));

        DropForeignKey("dbo.Policies", "AccountId", "dbo.Accounts");
        DropIndex("dbo.Policies", new[] { "AccountId" });
        DropTable("dbo.Policies");

        Sql($"DELETE FROM Accounts WHERE ID = '{accountIds[0]}'");
        Sql($"DELETE FROM Accounts WHERE ID = '{accountIds[1]}'");

        DropTable("dbo.Accounts");
    }
}

警告:如果您的任何迁移具有创建新 DC 并执行某些数据库更新的 .NET 代码,那么在合并迁移时这些可能不起作用。例如,如果迁移 1 添加了 Account table,而迁移 2 使用 .NET 代码将记录插入到 Account 中,它将在组合迁移中崩溃,因为从技术上讲,Account 尚未创建。用 Sql('INSERT INTO...") 语句替换此类语句将解决此问题。