Ef Core

10.9. Наслідування (Inheritance Mapping)

10.9. Наслідування (Inheritance Mapping)

Вступ: Ієрархія класів → реляційна БД

C# має наслідування. SQL — ні. Як замаппити ієрархію класів на таблиці? EF Core пропонує три стратегії: TPH (одна таблиця), TPT (окрема таблиця для кожного типу) та TPC (таблиця для кожного конкретного класу).


Приклад: Ієрархія контенту

// Базовий клас
public abstract class Content
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public DateTime CreatedAt { get; set; }
    public bool IsPublished { get; set; }
}

// Конкретні типи
public class Article : Content
{
    public string Body { get; set; } = "";
    public string Author { get; set; } = "";
}

public class Video : Content
{
    public string Url { get; set; } = "";
    public int DurationSeconds { get; set; }
}

public class Podcast : Content
{
    public string AudioUrl { get; set; } = "";
    public string Host { get; set; } = "";
    public int EpisodeNumber { get; set; }
}

TPH (Table Per Hierarchy)

Одна таблиця для всієї ієрархії з Discriminator стовпцем:

CREATE TABLE Contents (
    Id                INT IDENTITY PRIMARY KEY,
    Discriminator     NVARCHAR(8) NOT NULL,  -- 'Article', 'Video', 'Podcast'
    Title             NVARCHAR(200) NOT NULL,
    CreatedAt         DATETIME2 NOT NULL,
    IsPublished       BIT NOT NULL,
    -- Article columns (NULL для Video/Podcast)
    Body              NVARCHAR(MAX) NULL,
    Author            NVARCHAR(200) NULL,
    -- Video columns
    Url               NVARCHAR(500) NULL,
    DurationSeconds   INT NULL,
    -- Podcast columns
    AudioUrl          NVARCHAR(500) NULL,
    Host              NVARCHAR(200) NULL,
    EpisodeNumber     INT NULL
);

Конфігурація:

public class ContentContext : DbContext
{
    public DbSet<Content> Contents => Set<Content>();
    public DbSet<Article> Articles => Set<Article>();
    public DbSet<Video> Videos => Set<Video>();
    public DbSet<Podcast> Podcasts => Set<Podcast>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // TPH — за замовчуванням
        modelBuilder.Entity<Content>()
            .HasDiscriminator<string>("ContentType")
            .HasValue<Article>("Article")
            .HasValue<Video>("Video")
            .HasValue<Podcast>("Podcast");
    }
}

Запити:

// Всі типи контенту
var all = context.Contents.ToList();
// SQL: SELECT * FROM Contents

// Тільки статті
var articles = context.Articles.ToList();
// SQL: SELECT * FROM Contents WHERE ContentType = 'Article'

// Поліморфний запит
var published = context.Contents
    .Where(c => c.IsPublished)
    .ToList();
// Результат може містити Article, Video, Podcast — C# автоматично створює правильний тип
Плюси TPHМінуси TPH
Найшвидше (без JOIN)NULL стовпці для інших типів
Простий SQLВеликі таблиці
Один запит — всі типиНе можна NOT NULL для підтипів

TPT (Table Per Type)

Окрема таблиця для кожного типу з JOIN:

modelBuilder.Entity<Content>().UseTptMappingStrategy();
// або
modelBuilder.Entity<Article>().ToTable("Articles");
modelBuilder.Entity<Video>().ToTable("Videos");
modelBuilder.Entity<Podcast>().ToTable("Podcasts");
-- Три таблиці + базова
CREATE TABLE Contents  (Id, Title, CreatedAt, IsPublished);
CREATE TABLE Articles  (Id FK→Contents, Body, Author);
CREATE TABLE Videos    (Id FK→Contents, Url, DurationSeconds);
CREATE TABLE Podcasts  (Id FK→Contents, AudioUrl, Host, EpisodeNumber);
Плюси TPTМінуси TPT
Нормалізовано (без NULL)JOIN при кожному запиті
NOT NULL для підтипівПовільніше за TPH

TPC (Table Per Concrete class, EF Core 7+)

Окрема таблиця для кожного конкретного класу (без базової таблиці):

modelBuilder.Entity<Content>().UseTpcMappingStrategy();
CREATE TABLE Articles  (Id, Title, CreatedAt, IsPublished, Body, Author);
CREATE TABLE Videos    (Id, Title, CreatedAt, IsPublished, Url, DurationSeconds);
CREATE TABLE Podcasts  (Id, Title, CreatedAt, IsPublished, AudioUrl, Host, EpisodeNumber);
-- Без базової таблиці Contents!
Плюси TPCМінуси TPC
Немає JOINUNION ALL для полоіморфних запитів
Кожна таблиця самодостатняДублювання базових стовпців

Порівняння стратегій

КритерійTPHTPTTPC
Таблиць1N+1N
NULL стовпціТакНіНі
JOINНіТакНі
Поліморфні запитиШвидкоПовільно (JOIN)UNION ALL
Конкретний типДе Discriminator = XJOIN 2 таблицьОдна таблиця
РекомендаціяЗа замовчуваннямРідкоОкремі випадки
Правило: Починайте з TPH (за замовчуванням). Переходьте на TPC/TPT тільки якщо NULL-стовпці стають проблемою або потрібна строга нормалізація.

Global Query Filters

Фільтри, що автоматично додаються до всіх запитів:

// Soft Delete
modelBuilder.Entity<Content>()
    .HasQueryFilter(c => !c.IsDeleted);

// Multi-tenancy
modelBuilder.Entity<Content>()
    .HasQueryFilter(c => c.TenantId == _currentTenantId);

Практичні завдання

Рівень 1: Базовий

Завдання 1.1: TPH

  1. Payment (базовий) → CreditCardPayment, BankTransferPayment, CryptoPayment.
  2. Конфігурація Discriminator.
  3. Додайте по 3 платежі кожного типу.
  4. Запити: всі, тільки CreditCard, сума по типу.

Завдання 1.2: Порівняння

  1. Реалізуйте ту саму ієрархію через TPT.
  2. Порівняйте згенерований SQL для полоіморфного запиту.

Резюме

TPH

Одна таблиця, Discriminator. Найшвидше, за замовчуванням. NULL для стовпців інших типів.

TPT

Окрема таблиця + JOIN. Нормалізовано, але повільно через JOIN.

TPC

Самодостатні таблиці. UNION ALL для поліморфних запитів. EF Core 7+.

Global Query Filters

Автоматичні WHERE для soft delete, multi-tenancy. IgnoreQueryFilters() для обходу.
Copyright © 2026