Поделиться через


Наследство

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. Например, столбец RssUrl имеет значение NULL, так как обычные экземпляры Blog не имеют этого свойства.

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

Подсказка

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

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

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

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

снимок экрана: результаты запроса иерархии сущностей блога с помощью шаблона table-per-hierarchy

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

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("blog_type")
        .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 все типы сопоставляются с отдельными таблицами. Каждая таблица содержит столбцы для всех свойств соответствующего типа сущности. Это устраняет некоторые распространенные проблемы с производительностью стратегии 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, за исключением того, что для каждого конкретного типа в иерархии создается другая таблица, но таблицы не созданные для абстрактных типов , поэтому имя "таблица на конкретный тип". Как и в случае с 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# не разрешает экземпляры абстрактных типов, поэтому нет ситуаций, когда экземпляр абстрактного типа будет сохранен в базе данных.

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

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

таблица с кошками

Идентификатор Имя FoodId Ветеринар Уровень образования
1 Алиса 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли магистр делового администрирования (MBA)
2 Мак 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли Детский сад
8 Бакстер 5dc5019e-6f72-454b-d4b0-08da7aca624f Ветеринарная клиника Ботелл бакалавр наук

Таблица "Собаки"

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

таблица FarmAnimals

Идентификатор Имя FoodId Ценность Вид
4 Клайд 1d495075-f527-4498-d4af-08da7aca624f 100.00 Осёл африканский (Equus africanus asinus)

таблица "Человеки"

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

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

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

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

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

Ситуация становится немного более сложной для TPC. Во-первых, важно понимать, что EF Core требует, чтобы все сущности в иерархии имели уникальное значение ключа, даже если сущности имеют разные типы. Например, используя нашу модель-пример, собака не может иметь то же значение ключа идентификатора, что и кошка. Во-вторых, в отличие от 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. Однако в любой базе данных, включая SQLite, поддерживается генерация на стороне клиента или глобально уникальные идентификаторы (GUID).

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

Стратегия сопоставления 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, только если это ограничено внешними факторами.