在将迁移用于一对一关系到数据库时,Entity Framework生成的迁移会崩溃

时间:2018-03-22 12:08:42

标签: c# entity-framework entity-framework-6

我有一个相当大的模型,我们不得不改变外键关系。除了改变关系外,迁移不应该做太多。但是,我们观察到的行为是生成的迁移是虚假的,事实上,它试图删除表的主键。这显然失败了。

旧的情况如下:

public class InvoiceStatus
{
    public int Id { get; set; }
}

public class InvoiceSample 
{
    public int Id { get; set; }

    public int InvoiceStatusId { get; set; }

    public InvoiceStatus InvoiceStatus { get; set; }
}

现在已改为:

public class InvoiceStatus
{
    public int Id { get; set; }

    public int? InvoiceSampleId { get; set; }

    public virtual InvoiceSample InvoiceSample{ get; set; }
}

public class InvoiceSample 
{
    public int Id { get; set; }

    public int InvoiceStatusId { get; set; }

    public InvoiceStatus InvoiceStatus { get; set; }
}

除了更改实体本身之外,我们还将以下代码段添加到ModelBuilder

        modelBuilder.Entity<InvoiceStatus>()
                            .HasOptional(st => st.InvoiceSample)
                            .WithRequired(smp => smp.InvoiceStatus);

为此更改生成的迁移包含以下行:

    public override void Up()
    {
        DropForeignKey("dbo.InvoiceSamples", "InvoiceStatusId", "dbo.InvoiceStatus");
        DropIndex("dbo.InvoiceSamples", new[] { "InvoiceStatusId" });
        DropColumn("dbo.InvoiceSamples", "Id");
        RenameColumn(table: "dbo.InvoiceSamples", name: "InvoiceStatusId", newName: "Id");
        DropPrimaryKey("dbo.InvoiceSamples");
        AddColumn("dbo.InvoiceStatus", "InvoiceSampleId", c => c.Int());
        AlterColumn("dbo.InvoiceSamples", "Id", c => c.Int(nullable: false, identity: true));
        AddPrimaryKey("dbo.InvoiceSamples", "Id");
        CreateIndex("dbo.InvoiceSamples", "Id");
        CreateIndex("dbo.InvoiceSamples", "InvoiceStatusId", unique: true);
        AddForeignKey("dbo.InvoiceStatus", "SetId", "dbo.InvoiceSets", "Id", cascadeDelete: true);
        AddForeignKey("dbo.InvoiceSamples", "Id", "dbo.InvoiceStatus", "Id");;
    }

显然,这种迁移不会起作用,因为EF会在主键仍然是主键的情况下尝试删除主键。

此外,我在上述迁移后尝试生成的迁移在执行时也失败了。第二次迁移是删除两个实体之间的外键关系。在数据库上逐个手动执行生成的SQL语句时,自动迁移失败并出现SQL异常。

在这种情况下,EF是否有足够的理由生成并执行有效的迁移?或者我在这里缺少什么?

1 个答案:

答案 0 :(得分:1)

我将在这里做一些假设(这将有助于查看表格中的当前列以确认我做出正确的假设)。

一对一关系具有主体和从属实体。在物理表中,从属表包括一个到主表的FK的列。首先创建主表中的寄存器,它可以在依赖表中没有相关寄存器的情况下存在。因此,在您更新的模型中,InvoiceStatus是主体,InvoiceSample是依赖实体。

EF6实现这样的一对一关系:主表具有PK,依赖表具有PK,该PK也是主表的FK列。所以两个PK字段中的值都是相同的。

所以EF希望你的表格是这样的:

InvoiceStatus: 
Column Id (PK)

InvoiceSamples:
Column Id (PK, FK to InvoiceStatus.Id)

但是看看你实体的先前版本,你可能现在有类似的东西(它实际上实现了一对多的关系,但你可能只有InvoiceSamples中每条记录最多只有一条记录在InvoiceStatus,所以它看起来像一对一):

InvoiceStatus: 
Column Id (PK)

InvoiceSamples:
Column Id (PK)
Column InvoiceStatusId (FK to InvoiceStatus.Id)

因此迁移尝试将现有表转换为上面第一个代码中显示的模式。问题是您已经有了现有记录,InvoiceSamples.Id中的值与InvoiceSamples.InvoiceStatusId的值不匹配。

如何解决这个问题?除非您的InvoiceSamples有其他FK并且InvoiceSamples.Id在其他地方使用,否则我会让EF删除现有的Id列并应用自动生成的迁移中包含的更改。如果您说出错,因为迁移会在删除PK约束之前尝试删除PK列,请尝试在DropPrimaryKey行之前移动DropColumn行。如果需要,您可以手动修改自动创建的迁移。

无论如何,在调整迁移之前,您应该检查InvoiceSampleInvoiceStatus中每条记录的真实记录是否真的没有。在这种情况下,除非先进行一些数据修复,否则您无法将关系转换为一对一。

您可以使用与此类似的查询:

select InvoiceStatus.Id, count(InvoiceSamples.Id) 
from InvoiceStatus
inner join InvoiceSamples
on InvoiceStatus.Id = InvoiceSamples.InvoiceStatusId
group by InvoiceStatus.Id
having count(InvoiceSamples.Id) > 1

更新:尝试以下迁移方法:

public override void Up()
{
    DropForeignKey("dbo.InvoiceSamples", "InvoiceStatusId", "dbo.InvoiceStatus");
    DropIndex("dbo.InvoiceSamples", new[] { "InvoiceStatusId" });
    DropPrimaryKey("dbo.InvoiceSamples");   //Moved up
    RenameColumn(table: "dbo.InvoiceSamples", name: "Id", newName: "OldId");    //Instead of DropColumn
    RenameColumn(table: "dbo.InvoiceSamples", name: "InvoiceStatusId", newName: "Id");  //This will be the new PK
    AlterColumn("dbo.InvoiceSamples", "Id", c => c.Int(nullable: false, identity: true));
    AddPrimaryKey("dbo.InvoiceSamples", "Id");
    CreateIndex("dbo.InvoiceSamples", "Id");
    AddForeignKey("dbo.InvoiceSamples", "Id", "dbo.InvoiceStatus", "Id");
    DropColumn("dbo.InvoiceSamples", "OldId");  //Now we can delete the old PK column
}

public override void Down()
{
    DropForeignKey("dbo.InvoiceSamples", "Id", "dbo.InvoiceStatus");
    DropIndex("dbo.InvoiceSamples", new[] { "Id" });
    DropPrimaryKey("dbo.InvoiceSamples");
    RenameColumn(table: "dbo.InvoiceSamples", name: "Id", newName: "InvoiceStatusId");
    AlterColumn("dbo.InvoiceSamples", "InvoiceStatusId", c => c.Int(nullable: false, identity: false));
    AddColumn("dbo.InvoiceSamples", "Id", c => c.Int(nullable: false, identity: true));
    AlterColumn("dbo.InvoiceSamples", "Id", c => c.Int(nullable: false, identity: true));
    AddPrimaryKey("dbo.InvoiceSamples", "Id");
    CreateIndex("dbo.InvoiceSamples", "InvoiceStatusId");
    AddForeignKey("dbo.InvoiceSamples", "InvoiceStatusId", "dbo.InvoiceStatus", "Id", cascadeDelete: true);
}

我在一个小型测试项目上测试过它们。我建议你这样做,以避免在每次迁移测试后恢复真实的大数据库的痛苦。

你可以通过几个步骤完成这个任务(相信我,它看起来很麻烦,但它并不是那么多):

  • 创建一个控制台应用程序项目。
  • 添加EF nuget包。
  • 在程序包管理器控制台中运行Enable-Migrations
  • 将连接字符串添加到app.config文件。像这样的东西(将在那里创建一个新的数据库):

      <connectionStrings>
        <add name="MyConnectionString" connectionString="Server=(localdb)\ProjectsV13;Database=EFSample;Trusted_Connection=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
      </connectionStrings>
    
  • 添加一个简单的DbContext,它在构造函数的硬编码参数中使用此连接字符串,并使用MigrateDatabaseToLatestVersion数据库初始化程序:

    public class TestDbContext : DbContext
    {
        public TestDbContext() : base("MyConnectionString")
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<TestDbContext, Migrations.Configuration>());
            Configuration.LazyLoadingEnabled = true;
        }
    
        public IDbSet<InvoiceStatus> InvoiceStatus { get; set; }
        public IDbSet<InvoiceSample> InvoiceSamples { get; set; }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
        }
    }
    
  • 在更改之前添加两个模型实体的简化版本(仅InvoiceStatusInvoiceSample,保持最小化)。使用Add-Migration(名称为Initial)为它们生成迁移。使用Update-Database应用迁移。将创建测试数据库。

  • 使用新属性(包括OnModelCreating代码中的新关系配置)更改实体,生成另一个迁移(例如,名称为Final)。使用我在更新中编写的代码修改该迁移。运行Update-Database。要返回上一个数据库版本,请运行Update-Database -TargetMigration Initial,或者只删除测试数据库。它将很容易再次创建。

作为最后一点,我测试了上面粘贴的迁移代码并且它有效,但我的表是空的,因此测试场景忽略了与重复数据相关的问题。它只测试迁移没有架构错误。您可以尝试在运行第二次迁移之前添加一些数据(手动或使用Seed类中的Configuration方法)。如果你导入真实表中的相同ID,那会更好。我的猜测是,在创建FK和索引时会出现错误,因为您有重复的ID,因为您之前的架构没有强制执行一对一的完整性。如果是这种情况,那么您必须保持当前的一对多关系,或者编写一些sql脚本来删除这些重复的值或其他一些方法来修复您的数据。我强烈建议您在实施更改之前使用我上面写的查询来确定您的数据是否已准备好应用一对一关系。如果您发现那些重复的ID,甚至不会尝试更改架构,直到您解决该问题为止。