在 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...") 语句替换此类语句将解决此问题。
既然我们中的一些人拥有 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...") 语句替换此类语句将解决此问题。