更改列的IDENTITY属性,需要删除并重新创建该列。

61

我正在使用 EF Core 2.1

这是我的初始模型定义。

public class Customer //Parent
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Email { get; set; }

    public BankAccount BankAccount { get; set; }

}


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

    public string Branch { get; set; }

    public string AcntNumber { get; set; }

    public DateTime CreatedDate { get; set; }

    public int CustomerId { get; set; }

    public Customer Customer { get; set; }

}

但我意识到拥有IdCustomerId两者都是一对一关系的开销,因此我可以将我的BankAccount模型定义更新如下。

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

    public string Branch { get; set; }

    public string AcntNumber { get; set; }

    public DateTime CreatedDate { get; set; }

    public Customer Customer { get; set; }

}

在 DbContext 类中,将主要实体定义如下。

HasOne(b => b.Customer).WithOne(c => c.BankAccount).HasForeignKey<BankAccount>(f => f.Id);

在运行 update-database 时,我遇到了以下错误。

System.InvalidOperationException:要更改列的 IDENTITY 属性,需要删除并重新创建该列。

然而,理想情况下,我不应该仅仅摆脱这个错误,我已经删除了列、约束以及表和整个数据库,但仍然出现相同的错误。


“...我意识到拥有Id和CustomerId两者是多余的,因为它们是一对一的关系。” Id是主键,而CustomerId是外键,那么这里的多余指的是什么? - Elyas Esna
@ElyasEsna,“一个客户一个银行账户”,因此在银行账户实体中,可以使用客户实体中的ID作为外键和主键。 - Kgn-web
似乎是一个问题 https://github.com/aspnet/EntityFrameworkCore/issues/7444,这是EF Core中的一个未解决问题。 - DevilSuichiro
2
给未来的回答者:人们不断地堆积“解决方案”到这个问题上,所有的解决方案都大致相同,但是没有一个真正适合生产环境,因为它不允许数据丢失并且有外键到主键。如果你有一个考虑到这一点的可靠解决方案,请发布它,否则请三思而后再添加更多噪音。 - Gert Arnold
1
@GertArnold 我的想法是,当你在生产环境中使用大量生产数据时,你可能不应该再使用迁移来更改数据库架构。自从代码优先方法问世以来,我一直担心基于代码模型的数据库更改自动化(缺乏更好的词汇)的依赖性。买家要谨慎。 - Vitoc
17个回答

37

我遇到了同样的问题,通过两个步骤和两个迁移来解决:

步骤1

  1. 删除identity列。
  2. 注释BankAccount中的ID并添加一个新的(即BankAccountId作为identity,添加迁移和更新-这将删除id)。
  3. 添加一个新的identity列。

步骤2

  1. 删除新增的列并重新添加之前的列。注释BankAccountId并取消注释ID。
  2. 添加迁移和更新(这会删除BankAccountId并将Id添加为identity)。

完美地工作了。 - Mohammed A. Fadil
19
仅适用于没有外键的超简单场景,不是一个可行的解决方案。 - Gert Arnold
这对我来说已经足够了,让我顺利通过了。谢谢。 - c-sharp-and-swiftui-devni

12

当我试图将模型从 public byte Id {get; set;} 更改为 public int Id {get; set;} 时,我遇到了这个问题。

为解决此问题,我采取了以下步骤:

  1. 在包管理器控制台中使用 Remove-Migration -Project <target_project> 命令删除所有迁移,直到目标模型的创建为止。
  2. 删除实际数据库。
  3. 如果您有一些未创建的中间迁移(例如它们来自另一个分支),请复制迁移文件和 ModelSnapshot 文件,并将它们粘贴到您的分支中(小心地覆盖它们!)。
  4. 在包管理器控制台中使用 add-migration <migration_name> 命令创建新的迁移。
  5. 在包管理器控制台中使用 update-database 命令更新数据库。

我能够通过这种方式解决问题,因为我的代码不是在生产环境中。如果模型已经存在于生产环境中,您可能需要面临其他复杂的问题。


26
如果你处于开发环境中,这种策略是完全有效的,但如果你处于预生产环境中,它会变得棘手。 - Falk

7

这个问题有两个部分的答案:

简而言之,请让EF为您完成。

首先:让EF处理关系

简单来说,身份属性是数据库用于管理专用ID列的属性,因此它不依赖于外部任何东西来识别每一行数据。因此,bankAccount类需要其自己的Id字段来具有标识属性;现在,如果您试图手动告诉EF如何处理关系,就像使用以下代码:

    HasOne(b => b.Customer).WithOne(c => c.BankAccount).HasForeignKey<BankAccount>(f => f.Id);

你所做的是覆盖了EF本身的内部逻辑,在这种情况下,你告诉EF银行账户ID是引用客户的字段,但实际上不是。因此EF尝试从银行账户模型中删除ID字段的IDENTITY属性,但这只是因为你告诉它银行账户ID应该与客户ID相同
我认为我理解你想表达的一对一关系,但如果客户想再开一个银行账户会发生什么呢?D:
相反,你应该保留int CustomerId字段并删除Customer Customer字段,不仅更清晰,而且EF将会自动识别两个类之间的关系,只要有一个名为Customer的类和一个Id字段即可。
   public int CustomerId { get; set; } //keep

   public Customer Customer { get; set; } //delete

因此,客户可以在银行开立任意数量的账户,而表格不会受到影响,EF知道该怎么做。

它会自动生成相应的外键并将其添加到迁移文件中,就像这样:

    migrationBuilder.AddForeignKey(
            name: "FK_BankAccount_Customer_CustomerId",
            table: "BankAccount",
            column: "CustomerId",
            principalTable: "Customer",
            principalColumn: "Id",
            onDelete: ReferentialAction.Cascade);

这将反映出一对多的关系,这很好。

现在回答问题,如果您仍然想删除属性:

第二步:强制EF删除标识属性

(但它不会解决原问题的外键问题).

首先,简单回顾一下:要更改标识属性,数据库管理器(mysql、sqlserver等)总是会要求您删除并重新创建表,因为该字段是表的核心。所以您需要欺骗EF为您做一个变通。

  1. Add a second Id element to the model class with an obvious name like duplicateId.

    public class BankAccount
    {
        public int Id { get; set; }
        public int duplicateId { get; set; }
        ...
    }
    
  2. THIS IS THE TRICK ;)
    In the class there you implemented the DbContext interface, add the following method where you tell the ef which field you want the primary key to be:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BankAccount>().HasKey(x => new { x.duplicateId });
    }
    
  3. Add a new Migration with $ dotnet ef migrations add ModelIdChange1, since this changes the primary key of the table and the migration Up method should look lile this:

    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropPrimaryKey(
            name: "PK_BankAccount",
            table: "BankAccountTableName");
    
        migrationBuilder.AddColumn<int>(
            name: "duplicateId",
            table: "BankAccountTableName",
            type: "int",
            nullable: false,
            defaultValue: 0)
            .Annotation("SqlServer:Identity", "1, 1");
    
        migrationBuilder.AddPrimaryKey(
            name: "PK_BankAccount",
            table: "BankAccountTableName",
            column: "duplicateId");
    ...
    }
    
  4. Then do the database update with $ dotnet ef database update (these commands may vary, use whatever syntax you already used before).

  5. (OPTIONAL) If you are need to preserve the original IDs, check that they got preserved or simply do a dirty update on the table to copy the data from the Id field into the duplicateId.

  6. Now the original Id field is free to be deleted or updated, so go ahead and just delete de original field from the model:

    public class BankAccount
    {
        public int duplicateId { get; set; }
        ...
    }
    

如果你仍然在尝试强制执行将银行账户 ID 与客户 ID 相关联的原始命令,那么在此步骤运行该命令可能会起作用,但请不要这样做。

  1. And add a new Migration with $ dotnet ef migrations add ModelIdChange2, and then do the database update with $ dotnet ef database update, which deletes the original Id column and leaves duplicateId as the primary key.

  2. Now, the model looks almost the Original one but with a new identity column, you can leave it like that or just rename the field back from duplicateId to Id in the BankAccount class like this:

    public class BankAccount
    {
        public int Id { get; set; }
        ...
    }
    

    and do $ dotnet ef migrations add ModelIdChange3, and then do the database update with $ dotnet ef database update.


6

我需要:

  1. 从代码中完全注释掉表格
  2. 运行清除数据库的迁移
  3. 再次取消注释并更正映射
  4. 再次运行迁移

完成


3
只适用于没有外键的超级简单情况,不是一个真正可行的解决方案。 - Gert Arnold
不,已经有外键了。这就是问题发生的原因。 - Bassel
你是什么意思?这4个步骤不能与你删除的表中的外键一起使用。 - Gert Arnold
通过类似的方法解决了它;
  1. 从上下文中删除所有表(太多外键,而且除了种子数据之外仍是空数据库),
  2. 创建迁移
  3. 将所有内容放回上下文中
  4. 创建迁移 (本质上与仅删除所有迁移和数据库相同,但不想冒失丢失任何东西)
- Sam

4

当您想要更改架构或已存在的表格时,尝试修改现有表格时可能会出现此错误。EF Core 尚不支持它,需要手动操作。以下是您可以采取的措施:

  • 在迁移文件中注释相关代码以避免此错误。
  • 或者删除迁移文件并创建新的。
  • 删除上游迁移,并让迁移生成新代码。

3

对于像我这样懒的人:您想将名为“Id”的主键列的数据类型从 int 更改为 Guid,就像我的情况下操作表格名为"Translations"

  1. 在这种情况下,生成的迁移文件为:
migrationBuilder.AlterColumn<Guid>(
     name: "Id",
     table: "Translations",
     type: "uniqueidentifier",
     nullable: false,
     oldClrType: typeof(int),
     oldType: "int")
     OldAnnotation("SqlServer:Identity", "1, 1");

您可以删除或注释掉该内容

  1. 从"update-database"的错误中,我们知道System.InvalidOperationException:要更改列的IDENTITY属性,需要先删除并重新创建该列
  2. 我们还知道,在删除主键约束之前,无法删除该列。我们的新迁移变成了:
migrationBuilder.DropPrimaryKey(
    name: "PK_Translations",
    table: "Translations");
  migrationBuilder.DropColumn(
    name: "Id",
    table: "Translations");
  migrationBuilder.AddColumn<Guid>(
    name: "Id",
    table: "Translations",
    type: "uniqueidentifier",
    nullable: false);

记得在Down覆盖方法中执行相反的操作,以防您需要撤销迁移。


1
这个回答如何解决问题? - Gert Arnold

2

在我的情况下,表SharedBalances被重命名为Balances,其自增列SharedBalancesId被重命名为BalanceId。 SQL命令在SQL Server上执行。您还可以尝试migrationBuilder.Sql(my_sql_command_here)。

我创建了迁移并得到了相同的错误。

使用TSQL命令重命名列和表:

EXEC sp_RENAME 'SharedBalances.SharedBalanceId', 'BalanceId', 'COLUMN';

EXEC sp_RENAME 'SharedBalances', 'Balances';

-- Caution: Changing any part of an object name could break scripts and stored procedures.

在您的迁移中注释RenameTable命令:

/*
migrationBuilder.RenameTable(
    name: "SharedBalances",
    newName: "Balances");
*/

在迁移中注释AddPrimaryKey命令:

/*
migrationBuilder.DropPrimaryKey(
    name: "PK_SharedBalances",
    table: "Balances");
migrationBuilder.AddPrimaryKey(
    name: "PK_Balances",
    table: "Balances",
    column: "BalanceId");
*/

在您的迁移中更新表名 DropForeignKey 命令的出现:

从这个...

        migrationBuilder.DropForeignKey(
            name: "FK_SharedBalances_Users_OwnerUserId",
            table: "SharedBalances");

        migrationBuilder.DropPrimaryKey(
            name: "PK_SharedBalances",
            table: "SharedBalances");

变为:

        migrationBuilder.DropForeignKey(
            name: "FK_SharedBalances_Users_OwnerUserId",
            table: "Balances");

        migrationBuilder.DropPrimaryKey(
            name: "PK_SharedBalances",
            table: "Balances");

现在您的迁移将会成功。以下是它的实现方式:


2
注意:我正在使用.NET 6和EF 6.0.5。
我收到了这个错误,但情况并不完全相同。我有2个模型,它们之间是一对多的关系。在具有多个关系的模型上,我意识到我将外键属性标记为键,因此它既是主键也是外键和标识。我不想要这样。
我将[Key]注释更改为我想要的字段,并创建了一个迁移。当我运行迁移时,我收到了这个错误。我通过在新迁移中注释掉AlterColumn语句并将Identity注释添加到PK字段中来解决了这个问题。
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropPrimaryKey(
            name: "PK_EmailListAddresses",
            table: "EmailListAddresses");

        //migrationBuilder.AlterColumn<int>(
        //    name: "AddressId",             //Property I wanted to make the new PK
        //    table: "EmailListAddresses",   //Class/Entity Name
        //    type: "int",
        //    nullable: false,
        //    oldClrType: typeof(int),
        //    oldType: "int")
        //    .Annotation("SqlServer:Identity", "1, 1");

        //Added annotation here
        migrationBuilder.AddPrimaryKey(
            name: "PK_EmailListAddresses",
            table: "EmailListAddresses",
            column: "AddressId").Annotation("SqlServer:Identity", "1, 1");

        migrationBuilder.CreateIndex(
            name: "IX_EmailListAddresses_EmailListId",
            table: "EmailListAddresses",
            column: "EmailListId");
    }

从这里开始,我运行了update-database命令,一切都按照预期工作。AddressId成为主键,EmailListId仍然是外键。


2

我遇到了同样的问题(在我的情况下,表中没有任何数据),我用以下方式解决了它(这不是正确的方法,但对我有效):

  1. 我手动从EFCore项目中删除了迁移。我还从文件_ContextModelSnapshot中删除了添加的那些行。(我有一个已应用的迁移和一个已创建但未应用的迁移,因为我收到了一个错误-更改列的IDENTITY属性,需要删除并重新创建该列)
  2. 我手动删除了在数据库中创建的表(由第一个迁移创建)
  3. 我删除了与我想要删除的迁移相关联的_EFMigrationHistory表中的一行。
  4. 重新运行VS
  5. Add-Migration NewOneCleanMigration
  6. Update-Database

2
在我看来,除了开发数据库之外,运行EF Migrations对任何其他数据库都是在招惹麻烦。因为你自然而然地受到限制,EF Migrations有时会在更改对象结构(更改主键和更改外键是最常见的)时直接拒绝工作。
多年来,我一直使用工具来确保将DB模式包含在版本控制中(与EF Migrations相补充)。首先在开发数据库上进行开发更改(数据不重要),创建多个迁移,然后使用工具将它们合并成一个DB部署脚本。
这是我在这种情况下会做的事情的摘要:
1. 删除(注释掉)所有旧类BankAccount的引用 2. 创建迁移并应用于开发数据库 3. 重新添加BankAccount类及其更正的定义 4. 创建迁移并应用于开发数据库 5. 使用DB比较工具(我偏爱APEX SQL Diff,但市场上还有其他工具)创建一个汇总两个迁移的部署脚本。 6. 在测试环境上测试此脚本(应该有一些数据) 7. 如果测试结果良好,则应用于生产环境
事实上,如果您想用代码优先方法彻底更改生产数据的结构,除非您了解并解决了从一种结构迁移到另一种结构的数据迁移,否则这可能会对您产生不良影响。

多年来,我使用了许多工具... 请问您使用了哪些工具? - arantar
1
ApexSQL是我首选的工具,用于比较数据库架构和数据以帮助部署过程。他们有各种购买选项和完全功能的免费试用版。Red Gate也提供了类似的产品,名为“SQL Compare”。 - Cueball 6118
1
任何围绕着代码优先迁移的回答(即使是MS Docs团队告诉我的),都只适用于学生项目和教程。在真实的现场数据库环境中,这是一个可怕的玩笑。@Cueball6118上面所回答的才是真正的答案(无论您使用Apex等、Visual Studio DB Comparison还是仅使用SQL脚本)。我们支持现场数据库实例(MSSQL),并建议新手放弃这些教程级别的做法,以避免工作中的不眠之夜。即使是看起来完美的迁移,在现场环境中也可能导致无法接受的停机时间。 - Ajay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接