迁移会创建额外的键和索引

问题描述 投票:0回答:1

我最近一直在通过 EF Core 7.0.4 设置代码优先数据库,但在运行迁移时遇到奇怪的行为 -> 正在生成额外的备用键和索引,但我不明白。让我引导您完成它:

我首先编写了一个用于设计和构建数据库结构(表、约束等)的 SQL 脚本。这是我的 SQL 脚本的模拟:

CREATE TABLE [Product].[Family] 
(
    uid uniqueidentifier 
        CONSTRAINT [DF_Product_Family_UID] DEFAULT (NEWSEQUENTIALID()) ROWGUIDCOL NOT NULL,
    code int IDENTITY (1, 1) NOT FOR REPLICATION NOT NULL,
    description nvarchar(50) NOT NULL,
    created datetime 
        CONSTRAINT [DF_Product_Family_Created] DEFAULT GETDATE() NOT NULL,
    updated datetime 
        CONSTRAINT [DF_Product_Family_Updated] DEFAULT GETDATE() NOT NULL,
    inactive bit 
        CONSTRAINT [DF_Product_Family_Inactive] DEFAULT (0) NOT NULL,

    CONSTRAINT [PK_Product_Family_UID] PRIMARY KEY NONCLUSTERED (uid ASC)
)

CREATE UNIQUE CLUSTERED INDEX [IX_Product_Family_Code] 
ON [Product].[Family] (code ASC)
GO

CREATE TABLE [Product].[Category] 
(
    uid uniqueidentifier 
        CONSTRAINT [DF_Product_Category_UID] DEFAULT (NEWSEQUENTIALID()) ROWGUIDCOL NOT NULL,
    code int IDENTITY (1, 1) NOT FOR REPLICATION NOT NULL,
    description nvarchar(50) NOT NULL,
    fFamilyCode int NOT NULL,
    created datetime 
        CONSTRAINT [DF_Product_Category_Created] DEFAULT GETDATE() NOT NULL,
    updated datetime 
        CONSTRAINT [DF_Product_Category_Updated] DEFAULT GETDATE() NOT NULL,
    inactive bit 
        CONSTRAINT [DF_Product_Category_Inactive] DEFAULT (0) NOT NULL,

    CONSTRAINT [PK_Product_Category_UID] PRIMARY KEY NONCLUSTERED (uid ASC)
)

CREATE UNIQUE CLUSTERED INDEX [IX_Product_Category_Code] 
ON [Product].[Category] (code ASC)
GO

ALTER TABLE [Product].[Category]
    ADD CONSTRAINT [FK_Category_Product_Family] 
        FOREIGN KEY (fFamilyCode) REFERENCES [Product].[Family] (code)

然后,我将这些 SQL 实体复制到项目解决方案中的模型中:

public partial class Family
{
    [Column(Order = 0)]
    public Guid Uid { get; set; }
    [Column(Order = 1)]
    public int Code { get; set; }
    [Column(Order = 2)]
    public string Description { get; set; } = null!;
    [Column(Order = 3)]
    public DateTime Created { get; set; }
    [Column(Order = 4)]
    public DateTime Updated { get; set; }
    [Column(Order = 5)]
    public bool Inactive { get; set; }

    public virtual ICollection<Category> Categories { get; } = new List<Category>();

    public virtual ICollection<Product> Products { get; } = new List<Product>();

    public virtual ICollection<SubCategory> SubCategories { get; } = new List<SubCategory>();
}

public partial class Category
{
    [Column(Order = 0)]
    public Guid Uid { get; set; }
    [Column(Order = 1)]
    public int Code { get; set; }
    [Column(Order = 2)]
    public string Description { get; set; } = null!;
    [Column(Order = 3)]
    public int FFamilyCode { get; set; }
    [Column(Order = 4)]
    public DateTime Created { get; set; }
    [Column(Order = 5)]
    public DateTime Updated { get; set; }
    [Column(Order = 6)]
    public bool Inactive { get; set; }

    public virtual Family FFamilyCodeNavigation { get; set; } = null!;
    public virtual ICollection<SubCategory> SubCategories { get; } = new List<SubCategory>();

    public virtual ICollection<Product> Products { get; } = new List<Product>();
}

最后是对应的数据库集:

modelBuilder.Entity<Family>(entity =>
{
    entity.ToTable("Family", "Product");

    entity.HasKey(e => e.Uid)
        .HasName("PK_Product_Family_UID")
        .IsClustered(false);

    entity.HasIndex(e => e.Code, "IX_Product_Family_Code")
        .IsClustered();

    entity.Property(e => e.Uid)
        .HasDefaultValueSql("(newsequentialid())")
        .HasColumnName("uid");

    entity.Property(e => e.Code)
        .UseIdentityColumn(1, 1)
        .HasColumnName("code");

    entity.Property(e => e.Description)
        .HasMaxLength(50)
        .HasColumnName("description");

    entity.Property(e => e.Created)
        .HasDefaultValueSql("(getdate())")
        .HasColumnType("datetime")
        .HasColumnName("created");

    entity.Property(e => e.Updated)
        .HasDefaultValueSql("(getdate())")
        .HasColumnType("datetime")
        .HasColumnName("updated");

    entity.Property(e => e.Inactive)
        .HasDefaultValue(false)
        .HasColumnName("inactive");
});

modelBuilder.Entity<Category>(entity =>
{
    entity.ToTable("Category", "Product");

    entity.HasKey(e => e.Uid)
        .HasName("PK_Product_Category_UID")
        .IsClustered(false);

    entity.HasIndex(e => e.Code, "IX_Product_Category_Code")
        .IsUnique()
        .IsClustered();

    entity.Property(e => e.Uid)
        .HasDefaultValueSql("(newsequentialid())")
        .HasColumnName("uid");
    
    entity.Property(e => e.Code)
        .UseHiLo()
        .UseIdentityColumn(1, 1)
        .HasColumnName("code");

    entity.Property(e => e.Description)
        .HasMaxLength(50)
        .HasColumnName("description");

    entity.Property(e => e.FFamilyCode)
        .HasColumnName("fFamilyCode");
    
    entity.Property(e => e.Created)
        .HasDefaultValueSql("(getdate())")
        .HasColumnType("datetime")
        .HasColumnName("created");

    entity.Property(e => e.Updated)
        .HasDefaultValueSql("(getdate())")
        .HasColumnType("datetime")
        .HasColumnName("updated");

    entity.Property(e => e.Inactive)
        .HasDefaultValue(false)
        .HasColumnName("inactive");

    entity.HasOne(d => d.FFamilyCodeNavigation).WithMany(p => p.Categories)
        .HasForeignKey(d => d.FFamilyCode)
        .HasPrincipalKey(d => d.Code)
        .OnDelete(DeleteBehavior.Restrict)
        .HasConstraintName("FK_Category_Product_Family");
});

现在,当我对

[Product].[Family]
表(不仅如此)运行迁移时,会生成另一个 AK 和一个额外的索引。此外,当涉及
[Product].[Category]
表时,需要一个额外的备用键,并且由于它引用
[Product].[Family]
表,因此它会创建另外两个索引,如以下屏幕截图所示:

由于我无法立即判断为什么会发生这种情况,因此想查看 EF Core 生成并针对我的服务器运行的实际 SQL 脚本。这是它:

CREATE TABLE [Product].[Family] 
(
    [uid] uniqueidentifier NOT NULL DEFAULT ((newsequentialid())),
    [code] int NOT NULL IDENTITY,
    [description] nvarchar(50) NOT NULL,
    [created] datetime NOT NULL DEFAULT ((getdate())),
    [updated] datetime NOT NULL DEFAULT ((getdate())),
    [inactive] bit NOT NULL DEFAULT CAST(0 AS bit),
    CONSTRAINT [PK_Product_Family_UID] PRIMARY KEY NONCLUSTERED ([uid]),
    CONSTRAINT [AK_Family_code] UNIQUE ([code])
);
GO

CREATE TABLE [Product].[Category] 
(
    [uid] uniqueidentifier NOT NULL DEFAULT ((newsequentialid())),
    [code] int NOT NULL IDENTITY,
    [description] nvarchar(50) NOT NULL,
    [fFamilyCode] int NOT NULL,
    [created] datetime NOT NULL DEFAULT ((getdate())),
    [updated] datetime NOT NULL DEFAULT ((getdate())),
    [inactive] bit NOT NULL DEFAULT CAST(0 AS bit),
    CONSTRAINT [PK_Product_Category_UID] PRIMARY KEY NONCLUSTERED ([uid]),
    CONSTRAINT [AK_Category_code] UNIQUE ([code]),
    CONSTRAINT [FK_Category_Product_Family] FOREIGN KEY ([fFamilyCode]) REFERENCES [Product].[Family] ([code]) ON DELETE NO ACTION
);
GO

CREATE INDEX [IX_Category_fFamilyCode] ON [Product].[Category] ([fFamilyCode]);
GO

CREATE UNIQUE CLUSTERED INDEX [IX_Product_Category_Code] ON [Product].[Category] ([code]);
GO

现在,我有了关于正在创建的额外索引的答案 - 因为

UNIQUE CONSTRAINT
但我真的不明白为什么 EF Core 会这样做,而且为什么它会创建额外的备用键?如果我只运行自己创建的 SQL 脚本而不是 EF Core 使用的 SQL 脚本,这两个表将/应该如下所示:

任何人都可以帮助我理解并解决它吗?

c# sql-server entity-framework-core ef-core-7.0
1个回答
1
投票

首先,删除

SubCategories
Products
集合,因为它们引用帖子中未包含的实体,并且仅关注
Family
Category
模型及其关系。

现在一些事实:

  1. EF Core 仅支持在主体实体中引用 key(主要或备用)的 FK。这与关系数据库不同,关系数据库只需要引用表中的唯一键
  2. EF Core 备用键与主键具有相同的要求 - 非空、插入后不可变并支持唯一约束/索引。与主键的唯一区别是备用键默认意味着非聚集索引。
  3. 每当您使用
    HasPrincipalKey
    API 时,如果引用的字段不是主键或备用键,EF 将为您创建备用键,并使用 默认常规约束/索引名称。

最后一个是在您的情况下导致意外的唯一约束和索引的原因。因为这条线

entity.HasOne(d => d.FFamilyCodeNavigation).WithMany(p => p.Categories)
    .HasForeignKey(d => d.FFamilyCode)
    .HasPrincipalKey(d => d.Code) // <--
    .OnDelete(DeleteBehavior.Restrict)
    .HasConstraintName("FK_Category_Product_Family");

即使您配置了唯一索引,它也没有使用常规名称,在本例中为“AK_Family_code”,因此 EF 会“为您”创建一个新名称。

有人可能认为这是一个错误,但事实就是如此,并且有一个简单的解决方案 - 不要让 EF 为您创建备用键,而是显式创建和配置此类键而不是唯一索引。

就你的情况而言,这是更换的问题

entity.HasIndex(e => e.Code, "IX_Product_Family_Code")
    .IsClustered();

entity.HasAlternateKey(e => e.Code)
    .HasName("IX_Product_Family_Code")
    .IsClustered();

现在生成的迁移应该是这样的

migrationBuilder.CreateTable(
    name: "Family",
    schema: "Product",
    columns: table => new
    {
        uid = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "(newsequentialid())"),
        code = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:Identity", "1, 1"),
        description = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
        created = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())"),
        updated = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())"),
        inactive = table.Column<bool>(type: "bit", nullable: false, defaultValue: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Product_Family_UID", x => x.uid)
            .Annotation("SqlServer:Clustered", false);
        table.UniqueConstraint("IX_Product_Family_Code", x => x.code)
            .Annotation("SqlServer:Clustered", true);
    });

migrationBuilder.CreateTable(
    name: "Category",
    schema: "Product",
    columns: table => new
    {
        uid = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "(newsequentialid())"),
        code = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:Identity", "1, 1"),
        description = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
        fFamilyCode = table.Column<int>(type: "int", nullable: false),
        created = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())"),
        updated = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())"),
        inactive = table.Column<bool>(type: "bit", nullable: false, defaultValue: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Product_Category_UID", x => x.uid)
            .Annotation("SqlServer:Clustered", false);
        table.ForeignKey(
            name: "FK_Category_Product_Family",
            column: x => x.fFamilyCode,
            principalSchema: "Product",
            principalTable: "Family",
            principalColumn: "code",
            onDelete: ReferentialAction.Restrict);
    });

migrationBuilder.CreateIndex(
    name: "IX_Category_fFamilyCode",
    schema: "Product",
    table: "Category",
    column: "fFamilyCode");

migrationBuilder.CreateIndex(
    name: "IX_Product_Category_Code",
    schema: "Product",
    table: "Category",
    column: "code",
    unique: true)
    .Annotation("SqlServer:Clustered", true);

和 DDL(使用

Script-Migration
命令提取):

CREATE TABLE [Product].[Family] (
    [uid] uniqueidentifier NOT NULL DEFAULT ((newsequentialid())),
    [code] int NOT NULL IDENTITY,
    [description] nvarchar(50) NOT NULL,
    [created] datetime NOT NULL DEFAULT ((getdate())),
    [updated] datetime NOT NULL DEFAULT ((getdate())),
    [inactive] bit NOT NULL DEFAULT CAST(0 AS bit),
    CONSTRAINT [PK_Product_Family_UID] PRIMARY KEY NONCLUSTERED ([uid]),
    CONSTRAINT [IX_Product_Family_Code] UNIQUE CLUSTERED ([code])
);
GO

CREATE TABLE [Product].[Category] (
    [uid] uniqueidentifier NOT NULL DEFAULT ((newsequentialid())),
    [code] int NOT NULL IDENTITY,
    [description] nvarchar(50) NOT NULL,
    [fFamilyCode] int NOT NULL,
    [created] datetime NOT NULL DEFAULT ((getdate())),
    [updated] datetime NOT NULL DEFAULT ((getdate())),
    [inactive] bit NOT NULL DEFAULT CAST(0 AS bit),
    CONSTRAINT [PK_Product_Category_UID] PRIMARY KEY NONCLUSTERED ([uid]),
    CONSTRAINT [FK_Category_Product_Family] FOREIGN KEY ([fFamilyCode]) REFERENCES [Product].[Family] ([code]) ON DELETE NO ACTION
);
GO

CREATE INDEX [IX_Category_fFamilyCode] ON [Product].[Category] ([fFamilyCode]);
GO

CREATE UNIQUE CLUSTERED INDEX [IX_Product_Category_Code] ON [Product].[Category] ([code]);
GO

这是所期望的。问题解决了。

请记住,即使数据库允许修改

Family.Code
列,EF core 也不会因为备用键要求/约束而不允许修改。

其中大部分内容均通过官方 EF Core 文档的备用键部分中的示例进行了解释。

© www.soinside.com 2019 - 2024. All rights reserved.