Как Entity Framework транслирует наследование классов C# в реляционные таблицы?

«Как Entity Framework транслирует наследование классов C# в реляционные таблицы?» — вопрос из категории Entity Framework, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Entity Framework Core (EF Core) поддерживает три основные стратегии маппинга наследования объектов на реляционные таблицы. Выбор стратегии влияет на производительность и сложность схемы БД.

1. TPH (Table Per Hierarchy) — Одна таблица для всей иерархии

  • Как работает: Все классы иерархии маппятся на одну таблицу. Создается дискриминаторная колонка (например, Discriminator), которая хранит тип объекта.
  • Плюсы: Простая схема БД, быстрые запросы без JOIN.
  • Минусы: Может привести к "раздутой" таблице с множеством NULL-колонок (свойства, специфичные для подклассов). Нарушает нормализацию.
  • Настройка (по умолчанию в EF Core):
public abstract class Vehicle
{
    public int Id { get; set; }
    public string Model { get; set; }
}

public class Car : Vehicle
{
    public int TrunkSize { get; set; } // Специфичное свойство
}

public class Motorcycle : Vehicle
{
    public bool HasSidecar { get; set; } // Специфичное свойство
}

// В контексте
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // TPH используется по умолчанию. Дискриминатор добавляется автоматически.
    modelBuilder.Entity<Vehicle>()
        .HasDiscriminator<string>("VehicleType")
        .HasValue<Car>("Car")
        .HasValue<Motorcycle>("Motorcycle");
}
Результирующая таблица Vehicles: Id Model VehicleType TrunkSize HasSidecar
1 Model S Car 500 NULL
2 Ninja Motorcycle NULL true

2. TPT (Table Per Type) — Таблица для каждого типа

  • Как работает: Для каждого класса в иерархии создается отдельная таблица. Таблица для базового класса содержит общие поля. Таблицы для производных классов содержат свои специфичные поля и внешний ключ на таблицу базового класса.
  • Плюсы: Нормализованная схема, нет NULL-колонок.
  • Минусы: Сложные запросы с JOIN, потенциально ниже производительность.
  • Настройка:
// В контексте
modelBuilder.Entity<Vehicle>().ToTable("Vehicles");
modelBuilder.Entity<Car>().ToTable("Cars"); // Специфичные свойства в отдельной таблице
modelBuilder.Entity<Motorcycle>().ToTable("Motorcycles");

3. TPC (Table Per Concrete Type) — Таблица для каждого конкретного типа

  • Как работает: Для каждого конкретного (не абстрактного) класса создается отдельная таблица, которая содержит ВСЕ его поля, включая унаследованные от родителя. Общие свойства дублируются в каждой таблице.
  • Плюсы: Избегает JOIN для запросов к конкретному типу.
  • Минусы: Дублирование данных (общие поля), сложности с генерацией уникальных ключей (Id) across tables.
  • Настройка (EF Core 7.0+):
modelBuilder.Entity<Vehicle>().UseTpcMappingStrategy();
modelBuilder.Entity<Car>().ToTable("Cars");
modelBuilder.Entity<Motorcycle>().ToTable("Motorcycles");

Выбор стратегии:

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