Недавно я работал над настройкой базы данных с кодом через 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]
(и не только), генерируется еще один АК и дополнительный индекс. Кроме того, когда дело доходит до таблицы [Product].[Category]
, появляется дополнительный альтернативный ключ, и поскольку он ссылается на таблицу [Product].[Family]
, он создает еще два индекса, как показано на этом снимке экрана:
Поскольку я не мог сразу сказать, почему это происходит, я решил увидеть реальный сценарий SQL, который EF Core генерирует и запускает на моем сервере. И вот оно:
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:
Может ли кто-нибудь помочь мне понять и исправить это?
Пожалуйста, позвольте мне прервать ваш ответ:
- EF Core поддерживает только FK, ссылающиеся на ключ (основной или альтернативный) в основной сущности. Это отличается от реляционной базы данных, которая требуется только уникальный ключ в указанной таблице.
Возможно, я сумасшедший или наивный, но разве я не должен был подпадать под правила реляционной базы данных?
- Альтернативный ключ EF Core имеет те же требования, что и первичный ключ: не нулевой, не изменяемый после вставки и подкрепленный уникальным ключом. ограничение/индекс. Единственное отличие от первичного ключа состоит в том, что альтернативные ключи по умолчанию подразумевают некластеризованный индекс.
Истинный.
- Всякий раз, когда вы используете HasPrincipalKey API, если указанное поле не является первичным или альтернативным ключом, EF создаст для вас альтернативный ключ. с обычными именами ограничений/индексов по умолчанию.
Да, я мог это видеть, но это нормально, меня не волнует сам ключ.
Итак, согласно вашей рекомендации изменить свойство с .HasIndex()
на .HasAlternateKey()
, я уже пробовал это раньше. Наборы баз данных теперь выглядят так:
Семья
modelBuilder.Entity<Family>(entity =>
{
entity.ToTable("Family", "Product");
entity.HasKey(e => e.Uid)
.HasName("PK_Product_Family_UID")
.IsClustered(false);
entity.HasAlternateKey(e => e.Code)
.HasName("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.HasAlternateKey(e => e.Code)
.HasName("IX_Product_Category_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.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");
});
Сгенерированная миграция
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.UniqueConstraint("IX_Product_Category_Code", x => x.code)
.Annotation("SqlServer:Clustered", true);
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");
И... наконец, результат миграции DDL-скрипта:
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 [IX_Product_Category_Code] UNIQUE CLUSTERED ([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
Как видите, в моем случае в DDL отсутствует UNIQUE CLUSTERED INDEX ON [Product].[Category]
по сравнению с тем, что вы получили.
В любом случае, я выполнил команду для обновления базы данных. И это выглядит так:
Выводы:
[Product].[Family]
, поскольку он создавался раньше.[Product].[Category]
, ориентированный на FK
(как показано на снимке экрана).[Product].[Category]
через SSMS и напишу для нее сценарий CREATE TO -> New query window
, чтобы увидеть реальный сценарий, стоящий за ней, это будет выглядеть так:/****** Object: Table [Product].[Category] Script Date: 14-03-2024 11:31:02 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Product].[Category](
[uid] [uniqueidentifier] NOT NULL,
[code] [int] IDENTITY(1,1) NOT NULL,
[description] [nvarchar](50) NOT NULL,
[fFamilyCode] [int] NOT NULL,
[created] [datetime] NOT NULL,
[updated] [datetime] NOT NULL,
[inactive] [bit] NOT NULL,
CONSTRAINT [PK_Product_Category_UID] PRIMARY KEY NONCLUSTERED
(
[uid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
CONSTRAINT [IX_Product_Category_Code] UNIQUE CLUSTERED
(
[code] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [Product].[Category] ADD DEFAULT (newsequentialid()) FOR [uid]
GO
ALTER TABLE [Product].[Category] ADD DEFAULT (getdate()) FOR [created]
GO
ALTER TABLE [Product].[Category] ADD DEFAULT (getdate()) FOR [updated]
GO
ALTER TABLE [Product].[Category] ADD DEFAULT (CONVERT([bit],(0))) FOR [inactive]
GO
ALTER TABLE [Product].[Category] WITH CHECK ADD CONSTRAINT [FK_Category_Product_Family] FOREIGN KEY([fFamilyCode])
REFERENCES [Product].[Family] ([code])
GO
ALTER TABLE [Product].[Category] CHECK CONSTRAINT [FK_Category_Product_Family]
GO
... не существует CONSTRAINT
, который бы генерировал IX_Category_fFamilyCode
неуникальный, некластеризованный индекс.
@Эльдар, я уже это говорил, я понял? Я просто хочу не создавать их, а воспроизводить поведение SQL
, а также избегать создания дополнительных AKs
Почему бы просто не сделать «uid» кластеризованного ПК и избавиться от лишнего ключа code
?
@DavidBrowne-Microsoft это требование для слияний и так далее. И да, проблема возникает, когда речь идет об индексах на основе столбца uniqueidentifier
.
Я не знаю, почему это немного меняет реализацию, но могу предложить гипотезу и способ ее проверки. В исходной реализации указано, что Product.Category.fFamilyCode
является внешним ключом, но в исходной таблице Product.Family
нет ключа, для которого он мог бы быть внешним ключом. Конечно, создание внешнего ключа для уникального индекса является законным, но структура сущности, по-видимому, говорит: «Если что-то является внешним ключом для этого столбца кода, то я собираюсь явно сделать это ключом, а не просто индексом». Это можно проверить, временно устранив ограничение внешнего ключа.
@allmhuran да, я понимаю. После создания unique constraint
автоматически создается файл index. Однако я получаю два индекса, созданные в таблице [Product].[Family]
. Почему так? Кроме того, EF Core
в конце концов генерирует скрипт для запуска на сервере, верно? Итак, теперь позвольте мне спросить вас — почему EF Core
генерирует сценарий, отличный от того, который я написал сам? Логика моего скрипта SQL
стандартная и соответственно.
«Как только будет создано уникальное ограничение, автоматически будет создан индекс» — действительно, но не имеет отношения к моему комментарию. Я предполагаю противоположную причинно-следственную связь: «поскольку был создан индекс и был создан внешний ключ к нему, EF преобразовал индекс в ограничение». Здесь есть тонкость. Да, уникальный индекс может по закону действовать как «внешняя» часть внешнего ключа, не будучи созданным в качестве ограничения, поддерживаемого автоматическим индексом. Но есть разница: в реализации EF значок AK_Category_code
появляется в папке «ключи». В вашей реализации это «просто» индекс.
@allmhuran, тогда как мне исправить такое поведение и избежать такого поведения?
Почему вы хотите это исправить? Действительно, я бы сказал, что реализация EF более точна семантически: «внешний ключ» — это «ключ во внешней (т. е. какой-то другой) таблице», поэтому он создал этот ключ как явный ключ, а не просто неявный. один.
@allmhuran, потому что у меня есть два созданных индекса, которые потребуют долгосрочного управления (т. е. фрагментации, перестроения и т. д.)? Я понимаю альтернативный ключ, это нормально, но я бы предпочел избегать большего количества индексов. И эти две таблицы — всего лишь пример, есть и другие таблицы, для которых EF создает больше индексов и, по сути, удваивает их.
Интересно, что произойдет, если в исходном сценарии вы сделаете code
первичный ключ кластеризованным на Family
и поместите уникальный некластеризованный индекс на uid
(вместо того, чтобы помещать некластеризованный первичный ключ на uid
и уникальный индекс на code
). Если моя гипотеза верна, то EF не будет создавать дополнительный элемент, поскольку внешний ключ будет ссылаться на то, что уже является явным ключом.
@allmhuran не пробовал, но даже не буду. Требования аналогичны приведенным выше, и EF Core не создает миграцию должным образом. Опять же, я могу понять дополнительный alternate key
, но я не хочу, чтобы создавался дополнительный индекс (индексы). Я просто не хочу их.
ХОРОШО. Иван дал ответ, подтверждающий мою гипотезу, и при моем подходе вы не получите никаких «лишних» индексов. Но если у вас есть какая-то другая причина сделать первоначальные требования к дизайну строго неизменными, несмотря на эти факты, то, думаю, это ваш ход.
Во-первых, давайте удалим коллекции 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 этого не делает из-за альтернативных ключевых требований/ограничений.
Большая часть этого объясняется на примерах в разделе Альтернативные ключи официальной документации EF Core.
Обновление: относительно «дополнительного» неуникального индекса в столбце FK («IX_Category_fFamilyCode»).
Создание неуникального индекса для столбцов FK считается хорошей практикой, поскольку оно помогает базе данных обеспечивать соблюдение ограничения FK, выполнять SQL-соединения для запроса дочерней «коллекции», каскадное удаление (при использовании) и т. д. EF строго следует этой практике и не позволяет вам нужно удалить такие индексы даже с помощью API-интерфейсов Fluent/Model. И, наверное, так и должно быть — эти индексы действительно помогают.
Пожалуйста, проверьте мои обновления.
Мои результаты не такие, как твои @Ivan. Я говорю о том, что я получу после запуска миграции и, следовательно, DDL-скрипт -> он не такой, как ваш. Кроме того, что вы думаете о том факте, что реальная таблица после обновления базы данных, когда я пишу ее определение, не включает этот дополнительный индекс, хотя он там есть. Что мне здесь не хватает?
Я не уверен. Миграция и DDL, которые я включил в ответ, были сгенерированы из точных классов (с закомментированными только вышеупомянутыми коллекциями навигации по коллекциям) и модифицированной свободной конфигурации из вашего сообщения в чистый контекст базы данных без чего-либо еще. Использование последней версии EF 8.0.3, если это имеет значение. Я не применил его к базе данных SqlServer, но предполагаю, что он выполнит тот же сценарий. Какая у вас версия EF (иногда это имеет значение)?
Я использую версию 7.03. Также, пожалуйста, проверьте еще раз мой вопрос, пункт 3 после обновления вашего ответа.
Я видел это. Кажется, возникла ошибка или проблема с командой «Script As -> CREATE TO», поскольку она не отображает фактический DDL. В то же время «Конструктор представлений» показывает правильный сценарий, который соответствует тому, что вы видите в древовидном представлении — узлы индексов и ключей для таблицы. Так что все хорошо. Что еще? Уникальный индекс «IX_Product_Family_Code» на самом деле не отсутствует, он создается неявно из «CONSTRAINT [IX_Product_Family_Code] UNIQUE CLUSTERED ([code] ASC)». И снова, не смотрите на базу данных, используйте команду EF Core `Script-Migration' PMC (или соответствующий CLI), чтобы получить DDL, который выполняется при миграции EF.
О вашем заявлении «Создание неуникального индекса для столбцов FK считается хорошей практикой» -> это имеет некоторые компромиссы, например, операции INSERT, UPDATE, DELETE
могут быть замедлены. Кроме того, требуется (потенциально) больше места для хранения и обслуживания. Я до сих пор не понимаю, почему EF
делает это только на основе рекомендации, а не придерживается основ SQL
и позволяет вам выполнять рекомендации по желанию. Вместо этого он их реализует, и вам необходимо вручную изменить/изменить файл migration
перед обновлением БД.
Я предоставил вам награду, поскольку ваш ответ в значительной степени объяснил, как работает EF
. Спасибо!
Это уникальный индекс, созданный для ограничения уникальности, что вполне ожидаемо. Если вы вручную создадите ограничение уникальности, оно также создаст уникальный индекс.