Миграции создают дополнительные ключи и индексы

Недавно я работал над настройкой базы данных с кодом через 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:

Может ли кто-нибудь помочь мне понять и исправить это?

Изменить ответ, полученный от Ивана

Пожалуйста, позвольте мне прервать ваш ответ:

  1. EF Core поддерживает только FK, ссылающиеся на ключ (основной или альтернативный) в основной сущности. Это отличается от реляционной базы данных, которая требуется только уникальный ключ в указанной таблице.

Возможно, я сумасшедший или наивный, но разве я не должен был подпадать под правила реляционной базы данных?

  1. Альтернативный ключ EF Core имеет те же требования, что и первичный ключ: не нулевой, не изменяемый после вставки и подкрепленный уникальным ключом. ограничение/индекс. Единственное отличие от первичного ключа состоит в том, что альтернативные ключи по умолчанию подразумевают некластеризованный индекс.

Истинный.

  1. Всякий раз, когда вы используете 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] по сравнению с тем, что вы получили.

В любом случае, я выполнил команду для обновления базы данных. И это выглядит так:

Выводы:

  1. Да, я избавился от лишнего индекса, созданного в таблице [Product].[Family], поскольку он создавался раньше.
  2. Я по-прежнему получаю дополнительный индекс в таблице [Product].[Category], ориентированный на FK (как показано на снимке экрана).
  3. Но что странно, если я щелкну правой кнопкой мыши по таблице [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 неуникальный, некластеризованный индекс.

  1. Я что-то упускаю из вашей рекомендации?

Это уникальный индекс, созданный для ограничения уникальности, что вполне ожидаемо. Если вы вручную создадите ограничение уникальности, оно также создаст уникальный индекс.

Eldar 10.03.2024 14:07

@Эльдар, я уже это говорил, я понял? Я просто хочу не создавать их, а воспроизводить поведение SQL, а также избегать создания дополнительных AKs

cdrrr 10.03.2024 14:21

Почему бы просто не сделать «uid» кластеризованного ПК и избавиться от лишнего ключа code?

David Browne - Microsoft 10.03.2024 20:53

@DavidBrowne-Microsoft это требование для слияний и так далее. И да, проблема возникает, когда речь идет об индексах на основе столбца uniqueidentifier.

cdrrr 10.03.2024 21:20

Я не знаю, почему это немного меняет реализацию, но могу предложить гипотезу и способ ее проверки. В исходной реализации указано, что Product.Category.fFamilyCode является внешним ключом, но в исходной таблице Product.Family нет ключа, для которого он мог бы быть внешним ключом. Конечно, создание внешнего ключа для уникального индекса является законным, но структура сущности, по-видимому, говорит: «Если что-то является внешним ключом для этого столбца кода, то я собираюсь явно сделать это ключом, а не просто индексом». Это можно проверить, временно устранив ограничение внешнего ключа.

allmhuran 10.03.2024 21:30

@allmhuran да, я понимаю. После создания unique constraint автоматически создается файл index. Однако я получаю два индекса, созданные в таблице [Product].[Family]. Почему так? Кроме того, EF Core в конце концов генерирует скрипт для запуска на сервере, верно? Итак, теперь позвольте мне спросить вас — почему EF Core генерирует сценарий, отличный от того, который я написал сам? Логика моего скрипта SQL стандартная и соответственно.

cdrrr 10.03.2024 21:39

«Как только будет создано уникальное ограничение, автоматически будет создан индекс» — действительно, но не имеет отношения к моему комментарию. Я предполагаю противоположную причинно-следственную связь: «поскольку был создан индекс и был создан внешний ключ к нему, EF преобразовал индекс в ограничение». Здесь есть тонкость. Да, уникальный индекс может по закону действовать как «внешняя» часть внешнего ключа, не будучи созданным в качестве ограничения, поддерживаемого автоматическим индексом. Но есть разница: в реализации EF значок AK_Category_code появляется в папке «ключи». В вашей реализации это «просто» индекс.

allmhuran 10.03.2024 23:49

@allmhuran, тогда как мне исправить такое поведение и избежать такого поведения?

cdrrr 11.03.2024 09:48

Почему вы хотите это исправить? Действительно, я бы сказал, что реализация EF более точна семантически: «внешний ключ» — это «ключ во внешней (т. е. какой-то другой) таблице», поэтому он создал этот ключ как явный ключ, а не просто неявный. один.

allmhuran 11.03.2024 14:59

@allmhuran, потому что у меня есть два созданных индекса, которые потребуют долгосрочного управления (т. е. фрагментации, перестроения и т. д.)? Я понимаю альтернативный ключ, это нормально, но я бы предпочел избегать большего количества индексов. И эти две таблицы — всего лишь пример, есть и другие таблицы, для которых EF создает больше индексов и, по сути, удваивает их.

cdrrr 11.03.2024 15:09

Интересно, что произойдет, если в исходном сценарии вы сделаете code первичный ключ кластеризованным на Family и поместите уникальный некластеризованный индекс на uid (вместо того, чтобы помещать некластеризованный первичный ключ на uid и уникальный индекс на code). Если моя гипотеза верна, то EF не будет создавать дополнительный элемент, поскольку внешний ключ будет ссылаться на то, что уже является явным ключом.

allmhuran 11.03.2024 20:38

@allmhuran не пробовал, но даже не буду. Требования аналогичны приведенным выше, и EF Core не создает миграцию должным образом. Опять же, я могу понять дополнительный alternate key, но я не хочу, чтобы создавался дополнительный индекс (индексы). Я просто не хочу их.

cdrrr 13.03.2024 15:51

ХОРОШО. Иван дал ответ, подтверждающий мою гипотезу, и при моем подходе вы не получите никаких «лишних» индексов. Но если у вас есть какая-то другая причина сделать первоначальные требования к дизайну строго неизменными, несмотря на эти факты, то, думаю, это ваш ход.

allmhuran 14.03.2024 00:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
13
257
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Во-первых, давайте удалим коллекции SubCategories и Products, поскольку они ссылаются на сущности, не включенные в пост, и сосредоточимся только на моделях Family и Category и их отношениях.

Теперь немного фактов:

  1. EF Core поддерживает только 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 этого не делает из-за альтернативных ключевых требований/ограничений.

Большая часть этого объясняется на примерах в разделе Альтернативные ключи официальной документации EF Core.

Обновление: относительно «дополнительного» неуникального индекса в столбце FK («IX_Category_fFamilyCode»).

Создание неуникального индекса для столбцов FK считается хорошей практикой, поскольку оно помогает базе данных обеспечивать соблюдение ограничения FK, выполнять SQL-соединения для запроса дочерней «коллекции», каскадное удаление (при использовании) и т. д. EF строго следует этой практике и не позволяет вам нужно удалить такие индексы даже с помощью API-интерфейсов Fluent/Model. И, наверное, так и должно быть — эти индексы действительно помогают.

Пожалуйста, проверьте мои обновления.

cdrrr 14.03.2024 10:37

Мои результаты не такие, как твои @Ivan. Я говорю о том, что я получу после запуска миграции и, следовательно, DDL-скрипт -> он не такой, как ваш. Кроме того, что вы думаете о том факте, что реальная таблица после обновления базы данных, когда я пишу ее определение, не включает этот дополнительный индекс, хотя он там есть. Что мне здесь не хватает?

cdrrr 14.03.2024 13:40

Я не уверен. Миграция и DDL, которые я включил в ответ, были сгенерированы из точных классов (с закомментированными только вышеупомянутыми коллекциями навигации по коллекциям) и модифицированной свободной конфигурации из вашего сообщения в чистый контекст базы данных без чего-либо еще. Использование последней версии EF 8.0.3, если это имеет значение. Я не применил его к базе данных SqlServer, но предполагаю, что он выполнит тот же сценарий. Какая у вас версия EF (иногда это имеет значение)?

Ivan Stoev 14.03.2024 14:26

Я использую версию 7.03. Также, пожалуйста, проверьте еще раз мой вопрос, пункт 3 после обновления вашего ответа.

cdrrr 14.03.2024 16:03

Я видел это. Кажется, возникла ошибка или проблема с командой «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.

Ivan Stoev 14.03.2024 16:15

О вашем заявлении «Создание неуникального индекса для столбцов FK считается хорошей практикой» -> это имеет некоторые компромиссы, например, операции INSERT, UPDATE, DELETE могут быть замедлены. Кроме того, требуется (потенциально) больше места для хранения и обслуживания. Я до сих пор не понимаю, почему EF делает это только на основе рекомендации, а не придерживается основ SQL и позволяет вам выполнять рекомендации по желанию. Вместо этого он их реализует, и вам необходимо вручную изменить/изменить файл migration перед обновлением БД.

cdrrr 18.03.2024 15:10

Я предоставил вам награду, поскольку ваш ответ в значительной степени объяснил, как работает EF. Спасибо!

cdrrr 18.03.2024 15:12

Другие вопросы по теме