我最近一直在通过 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 脚本,这两个表将/应该如下所示:
任何人都可以帮助我理解并解决它吗?
首先,删除
SubCategories
和 Products
集合,因为它们引用帖子中未包含的实体,并且仅关注 Family
和 Category
模型及其关系。
现在一些事实:
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 文档的备用键部分中的示例进行了解释。