Наследование

EF может сопоставить иерархию типов .NET с базой данных. Это позволяет создавать сущности .NET в коде как обычно, используя базовые и производные типы, и легко создавать соответствующую схему базы данных, выдавать запросы и т. д. Фактические сведения о сопоставлении иерархии типов зависят от поставщика; На этой странице описывается поддержка наследования в контексте реляционной базы данных.

Сопоставление иерархии типов сущностей

По соглашению EF не будет автоматически проверять базовые или производные типы; Это означает, что если требуется сопоставить тип СРЕДЫ CLR в иерархии, необходимо явно указать этот тип в модели. Например, указание только базового типа иерархии не приведет к неявно включению EF Core всех его подтипов.

В следующем примере предоставляется dbSet для Blog и его подкласса RssBlog. Если Blog имеется какой-либо другой подкласс, он не будет включен в модель.

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<RssBlog> RssBlogs { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

public class RssBlog : Blog
{
    public string RssUrl { get; set; }
}

Примечание.

Столбцы базы данных автоматически вносятся в значение NULL при использовании сопоставления TPH. Например, столбец имеет значение NULL, RssUrl так как обычные Blog экземпляры не имеют этого свойства.

Если вы не хотите предоставлять DbSet доступ к одной или нескольким сущностям в иерархии, можно также использовать API Fluent, чтобы убедиться, что они включены в модель.

Совет

Если вы не используете соглашения, можно явно указать базовый тип.HasBaseType Вы также можете удалить .HasBaseType((Type)null) тип сущности из иерархии.

Конфигурация таблицы на иерархию и дискриминатор

По умолчанию EF сопоставляет наследование с помощью шаблона TPH . TPH использует одну таблицу для хранения данных для всех типов в иерархии, а для определения типа каждой строки используется дискриминационный столбец.

Приведенная выше модель сопоставляется со следующей схемой базы данных (обратите внимание на неявно созданный Discriminator столбец, определяющий Blog тип, хранящийся в каждой строке).

Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern

Можно настроить имя и тип дискриминационных столбцов и значения, используемые для идентификации каждого типа в иерархии:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
}

В приведенных выше примерах EF добавил дискриминатор неявно в качестве теневого свойства в базовой сущности иерархии. Это свойство можно настроить как любое другое:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property("Discriminator")
        .HasMaxLength(200);
}

Наконец, дискриминация также может быть сопоставлена с обычным свойством .NET в вашей сущности:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator(b => b.BlogType);

    modelBuilder.Entity<Blog>()
        .Property(e => e.BlogType)
        .HasMaxLength(200)
        .HasColumnName("blog_type");
        
    modelBuilder.Entity<RssBlog>();
}

При запросе производных сущностей, использующих шаблон TPH, EF Core добавляет предикат над дискриминационным столбцом в запросе. Этот фильтр гарантирует, что в результате не будут получаться дополнительные строки для базовых типов или одноуровневых типов. Этот предикат фильтра пропускается для базового типа сущности, так как запрос базовой сущности получит результаты для всех сущностей в иерархии. При материализации результатов из запроса, если мы сталкиваемся с дискриминационным значением, которое не сопоставляется с любым типом сущности в модели, мы создадим исключение, так как мы не знаем, как материализовать результаты. Эта ошибка возникает только в том случае, если база данных содержит строки с дискриминационными значениями, которые не сопоставляются в модели EF. Если у вас есть такие данные, можно пометить дискриминационное сопоставление в модели EF Core как неполное, чтобы указать, что мы всегда должны добавлять предикаты фильтра для запроса любого типа в иерархии. IsComplete(false) Вызов дискриминационных конфигураций означает, что сопоставление будет неполным.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator()
        .IsComplete(false);
}

Общие столбцы

По умолчанию, если в иерархии два типа одноуровневых сущностей имеют свойство с одинаковым именем, они будут сопоставлены с двумя отдельными столбцами. Однако если их тип идентичен, их можно сопоставить с тем же столбцом базы данных:

public class MyContext : DbContext
{
    public DbSet<BlogBase> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Url)
            .HasColumnName("Url");

        modelBuilder.Entity<RssBlog>()
            .Property(b => b.Url)
            .HasColumnName("Url");
    }
}

public abstract class BlogBase
{
    public int BlogId { get; set; }
}

public class Blog : BlogBase
{
    public string Url { get; set; }
}

public class RssBlog : BlogBase
{
    public string Url { get; set; }
}

Примечание.

Поставщики реляционных баз данных, такие как SQL Server, не будут автоматически использовать предикат дискриминации при запросе общих столбцов при использовании приведения. Запрос Url = (blog as RssBlog).Url также вернет Url значение для строк с братом Blog . Чтобы ограничить запрос RssBlog сущностями, необходимо вручную добавить фильтр на дискриминатор, например Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Конфигурация табличного типа

В шаблоне сопоставления TPT все типы сопоставляются с отдельными таблицами. Как свойства базового типа, так и свойства производного типа хранятся в таблице, сопоставленной с этим типом. Таблицы, сопоставленные с производными типами, также хранят внешний ключ, который присоединяет производную таблицу к базовой таблице.

modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

Совет

Вместо вызова ToTable каждого типа сущности можно вызвать modelBuilder.Entity<Blog>().UseTptMappingStrategy() каждый тип корневой сущности, а имена таблиц будут созданы EF.

Совет

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

EF создаст следующую схему базы данных для приведенной выше модели.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
    CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

Примечание.

Если ограничение первичного ключа переименовано, новое имя будет применено ко всем таблицам, сопоставленным с иерархией, будущие версии EF позволят переименовать ограничение только для определенной таблицы при исправлении проблемы 19970 .

При использовании массовой конфигурации можно получить имя столбца для определенной таблицы, вызвав вызов GetColumnName(IProperty, StoreObjectIdentifier).

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    var tableIdentifier = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);

    Console.WriteLine($"{entityType.DisplayName()}\t\t{tableIdentifier}");
    Console.WriteLine(" Property\tColumn");

    foreach (var property in entityType.GetProperties())
    {
        var columnName = property.GetColumnName(tableIdentifier.Value);
        Console.WriteLine($" {property.Name,-10}\t{columnName}");
    }

    Console.WriteLine();
}

Предупреждение

Во многих случаях TPT показывает более низкое производительность по сравнению с TPH. Дополнительные сведения см. в документации по производительности.

Внимание

Столбцы для производного типа сопоставляются с разными таблицами, поэтому составные ограничения FK и индексы, использующие унаследованные и объявленные свойства, не могут быть созданы в базе данных.

Конфигурация типа таблицы на конкретный тип

Примечание.

Функция табличного типа (TPC) была представлена в EF Core 7.0.

В шаблоне сопоставления TPC все типы сопоставляются с отдельными таблицами. Каждая таблица содержит столбцы для всех свойств соответствующего типа сущности. Это устраняет некоторые распространенные проблемы с производительностью стратегии TPT.

Совет

Команда EF продемонстрировала и подробно говорила о сопоставлении TPC в эпизоде стенда сообщества данных .NET. Как и во всех эпизодах стенда сообщества, вы можете смотреть эпизод TPC сейчас на YouTube.

modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
    .ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
    .ToTable("RssBlogs");

Совет

ToTable Вместо вызова каждого типа сущности, просто вызываемого modelBuilder.Entity<Blog>().UseTpcMappingStrategy() для каждого типа корневой сущности, будут создаваться имена таблиц по соглашению.

Совет

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

EF создаст следующую схему базы данных для приведенной выше модели.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

Схема базы данных TPC

Стратегия TPC похожа на стратегию TPT, за исключением того, что для каждого конкретного типа в иерархии создается другая таблица, но таблицы не создаются для абстрактных типов, поэтому имя "table-per-concrete-type". Как и в случае с TPT, сама таблица указывает тип сохраненного объекта. Однако, в отличие от сопоставления TPT, каждая таблица содержит столбцы для каждого свойства в конкретном типе и его базовых типах. Схемы базы данных TPC денормализованы.

Например, рекомендуется сопоставить эту иерархию:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

При использовании SQL Server таблицы, созданные для этой иерархии, :

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

Обратите внимание на указанные ниже моменты.

  • Нет таблиц для Animal типов или Pet типов, так как они находятся abstract в объектной модели. Помните, что C# не разрешает экземпляры абстрактных типов, поэтому нет ситуаций, когда экземпляр абстрактного типа будет сохранен в базе данных.

  • Сопоставление свойств в базовых типах повторяется для каждого конкретного типа. Например, каждая таблица имеет столбец, и кошки и собаки имеют NameVet столбец.

  • Сохранение некоторых данных в этой базе данных приводит к следующему:

Таблица кошек

Идентификатор Имя FoodId Ветеринар EducationLevel
1 Алиса 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли МБ A
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли Дошкольного
8 Бакстер 5dc5019e-6f72-454b-d4b0-08da7aca624f Больница Ботелл Пет Bsc

Таблица собак

Идентификатор Имя FoodId Ветеринар Избранное
3 Тост 011aaf6f-d588-4fad-d4ac-08da7aca624f Пенджелли Г-н Белка

Таблица FarmAnimals

Идентификатор Имя FoodId Значение Разновидность
4 Клайд 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Таблица "Люди"

Идентификатор Имя FoodId ИзбранноеAnimalId
5 Венди 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Артур 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Кэти null 8

Обратите внимание, что в отличие от сопоставления TPT все сведения для одного объекта содержатся в одной таблице. В отличие от сопоставления TPH, в любой таблице нет сочетания столбцов и строк, где она никогда не используется моделью. Ниже показано, как эти характеристики могут быть важными для запросов и хранилища.

Создание ключей

Выбранная стратегия сопоставления наследования имеет последствия для создания и управления значениями первичного ключа. Ключи в TPH просты, так как каждый экземпляр сущности представлен одной строкой в одной таблице. Можно использовать любое поколение значений ключа, и никаких дополнительных ограничений не требуется.

Для стратегии TPT всегда есть строка в таблице, сопоставленной с базовым типом иерархии. Любое поколение ключей можно использовать в этой строке, а ключи для других таблиц связаны с этой таблицей с помощью ограничений внешнего ключа.

Вещи немного сложнее для TPC. Во-первых, важно понимать, что EF Core требует, чтобы все сущности в иерархии имели уникальное значение ключа, даже если сущности имеют разные типы. Например, используя нашу примерную модель, собака не может иметь то же значение ключа идентификатора, что и Cat. Во-вторых, в отличие от TPT, не существует общей таблицы, которая может выступать в качестве одного места, где живут ключевые значения и могут быть созданы. Это означает, что не удается использовать простой Identity столбец.

Для баз данных, поддерживающих последовательности, значения ключей можно создать с помощью одной последовательности, на которую ссылается ограничение по умолчанию для каждой таблицы. Это стратегия, используемая в таблицах TPC выше, где каждая таблица имеет следующую команду:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence — это последовательность баз данных, созданная EF Core. Эта стратегия используется по умолчанию для иерархий TPC при использовании поставщика базы данных EF Core для SQL Server. Поставщики баз данных для других баз данных, поддерживающих последовательности, должны иметь аналогичный по умолчанию. Другие стратегии создания ключей, использующие последовательности, такие как шаблоны Hi-Lo, также могут использоваться с TPC.

Хотя стандартные столбцы удостоверений не работают с TPC, можно использовать столбцы удостоверений, если каждая таблица настроена с соответствующим начальным и добавочным значением, чтобы значения, созданные для каждой таблицы, никогда не конфликтуют. Например:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

Важно!

Использование этой стратегии затрудняет добавление производных типов позже, так как для этого требуется общее количество типов в иерархии, которые будут известны заранее.

SQLite не поддерживает последовательности или начальное или добавочное значение удостоверения, поэтому создание целочисленных значений ключей не поддерживается при использовании SQLite со стратегией TPC. Однако в любой базе данных поддерживаются клиентские генерации или глобально уникальные ключи, такие как ИДЕНТИФИКАТОРЫ GUID, включая SQLite.

Ограничения внешнего ключа

Стратегия сопоставления TPC создает денормализованную схему SQL - это одна из причин, почему некоторые пуристы базы данных против него. Например, рассмотрим столбец FavoriteAnimalIdвнешнего ключа. Значение в этом столбце должно соответствовать значению первичного ключа некоторых животных. Это можно применить в базе данных с простым ограничением FK при использовании TPH или TPT. Например:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Но при использовании TPC первичный ключ для любого данного животного хранится в таблице, соответствующей конкретному типу этого животного. Например, первичный ключ кошки хранится в Cats.Id столбце, а первичный ключ собаки хранится в Dogs.Id столбце и т. д. Это означает, что для этой связи невозможно создать ограничение FK.

На практике это не проблема, если приложение не пытается вставить недопустимые данные. Например, если все данные вставляются EF Core и используют навигации для связи сущностей, то гарантируется, что столбец FK будет содержать допустимые значения PK в любое время.

Сводка и руководство

В итоге TPH обычно хорошо подходит для большинства приложений и является хорошим по умолчанию для широкого спектра сценариев, поэтому не добавляйте сложность TPC, если она не нужна. В частности, если ваш код будет в основном запрашивать сущности многих типов, например писать запросы к базовому типу, а затем опираться на TPH по TPC.

Это говорится, что TPC также является хорошей стратегией сопоставления для использования, когда ваш код будет в основном запрашивать сущности одного конечного типа, и ваши тесты показывают улучшение по сравнению с TPH.

Используйте TPT, только если это ограничено внешними факторами.