Ef Core
10.9. Наслідування (Inheritance Mapping)
10.9. Наслідування (Inheritance Mapping)
Вступ: Ієрархія класів → реляційна БД
C# має наслідування. SQL — ні. Як замаппити ієрархію класів на таблиці? EF Core пропонує три стратегії: TPH (одна таблиця), TPT (окрема таблиця для кожного типу) та TPC (таблиця для кожного конкретного класу).
Передумови: 10.3. Entity Configuration, 10.8. Міграції.
Приклад: Ієрархія контенту
// Базовий клас
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 |
|---|---|
| Немає JOIN | UNION ALL для полоіморфних запитів |
| Кожна таблиця самодостатня | Дублювання базових стовпців |
Порівняння стратегій
| Критерій | TPH | TPT | TPC |
|---|---|---|---|
| Таблиць | 1 | N+1 | N |
| NULL стовпці | Так | Ні | Ні |
| JOIN | Ні | Так | Ні |
| Поліморфні запити | Швидко | Повільно (JOIN) | UNION ALL |
| Конкретний тип | Де Discriminator = X | JOIN 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
Payment(базовий) →CreditCardPayment,BankTransferPayment,CryptoPayment.- Конфігурація Discriminator.
- Додайте по 3 платежі кожного типу.
- Запити: всі, тільки CreditCard, сума по типу.
Завдання 1.2: Порівняння
- Реалізуйте ту саму ієрархію через TPT.
- Порівняйте згенерований SQL для полоіморфного запиту.
Резюме
TPH
Одна таблиця, Discriminator. Найшвидше, за замовчуванням. NULL для стовпців інших типів.
TPT
Окрема таблиця + JOIN. Нормалізовано, але повільно через JOIN.
TPC
Самодостатні таблиці. UNION ALL для поліморфних запитів. EF Core 7+.
Global Query Filters
Автоматичні WHERE для soft delete, multi-tenancy. IgnoreQueryFilters() для обходу.