Ответ
Дискриминатор — это специальный столбец, который Entity Framework Core автоматически добавляет в таблицу базы данных при использовании стратегии наследования TPH (Table Per Hierarchy). Значение в этом столбце определяет, к какому конкретному типу-наследнику относится каждая строка в таблице.
Зачем это нужно? TPH позволяет хранить всю иерархию классов в одной таблице, что упрощает запросы (не нужны JOIN), но требует способа различать типы. Дискриминатор решает эту проблему.
Пример настройки:
// Модель
public abstract class BillingDetail
{
public int Id { get; set; }
public string Owner { get; set; }
public string Number { get; set; }
}
public class BankAccount : BillingDetail
{
public string BankName { get; set; }
public string Swift { get; set; }
}
public class CreditCard : BillingDetail
{
public int CardType { get; set; }
public string ExpiryMonth { get; set; }
}
// Конфигурация в DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BillingDetail>()
.HasDiscriminator<string>(nameof(BillingDetailType)) // Имя столбца
.HasValue<BankAccount>("BankAccount") // Значение для BankAccount
.HasValue<CreditCard>("CreditCard"); // Значение для CreditCard
}
Результирующая таблица в БД:
CREATE TABLE BillingDetails (
Id INT PRIMARY KEY,
Owner NVARCHAR(MAX),
Number NVARCHAR(MAX),
BankName NVARCHAR(MAX), -- NULL для CreditCard
Swift NVARCHAR(MAX), -- NULL для CreditCard
CardType INT, -- NULL для BankAccount
ExpiryMonth NVARCHAR(MAX), -- NULL для BankAccount
BillingDetailType NVARCHAR(MAX) NOT NULL -- Дискриминатор
);
Важные моменты:
- Имя столбца и значения можно кастомизировать, как показано выше.
- Тип дискриминатора может быть
string,int,enum. - При запросе к
DbSet<BillingDetail>EF Core автоматически добавляет в SQL условиеWHERE Discriminator = '...'в зависимости от того, запрашиваете вы базовый класс или конкретного наследника. - Недостаток TPH: Столбцы, специфичные для наследников, должны допускать
NULL, что может привести к "раздуванию" таблицы при глубоких иерархиях.
Ответ 18+ 🔞
Давай разжую про этот самый дискриминатор, а то звучит как что-то из инквизиции, а на деле — простая штука.
Представь, есть у тебя абстрактная сущность BillingDetail — типа платёжка какая-то. А от неё уже торчат два конкретных наследника: BankAccount (банковский счёт) и CreditCard (кредитка). И вот Entity Framework, такой хитрожопый, предлагает: "А давайте, ребята, мы всю эту семейку будем хранить в одной таблице в базе, а?" Это и есть стратегия TPH — одна таблица на всю иерархию.
И тут же встаёт вопрос: ну ладно, слили всё в одну кучу, но как потом, выгребая данные, понять — эта строка про банковский счёт или про кредитку? Вот для этого и вводится дискриминатор — специальный столбец-метка, который EF Core сам добавляет в таблицу и сам же им рулит.
По сути, это такой светофор в каждой строке. Значение "BankAccount" — значит, тут лежат данные счёта, "CreditCard" — ну, ты понял. Без этого столбца была бы полная анархия, все сущности перемешались, и ни черта не разберёшь.
Как это настраивается? Да элементарно, в переопределении OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BillingDetail>()
// Говорим: "Добавь столбец-дискриминатор с типом string и назови его 'BillingDetailType'"
.HasDiscriminator<string>("BillingDetailType")
// "Если это BankAccount — пиши в тот столбец 'BankAccount'"
.HasValue<BankAccount>("BankAccount")
// "Если CreditCard — пиши 'CreditCard'"
.HasValue<CreditCard>("CreditCard");
}
Что в базе получится? Одна большая таблица, где будут все поля от родителя и от всех детей, плюс наш волшебный столбец.
CREATE TABLE BillingDetails (
Id INT PRIMARY KEY,
Owner NVARCHAR(MAX), -- поле из BillingDetail
Number NVARCHAR(MAX), -- поле из BillingDetail
BankName NVARCHAR(MAX), -- поле из BankAccount (будет NULL для кредиток)
Swift NVARCHAR(MAX), -- поле из BankAccount (будет NULL для кредиток)
CardType INT, -- поле из CreditCard (будет NULL для счетов)
ExpiryMonth NVARCHAR(MAX), -- поле из CreditCard (будет NULL для счетов)
BillingDetailType NVARCHAR(MAX) NOT NULL -- Вот он, наш дискриминатор!
);
Как это работает в жизни? Когда ты пишешь context.BillingDetails.OfType<BankAccount>().ToList(), EF Core генерит умный SQL, который сам добавит условие WHERE BillingDetailType = 'BankAccount'. И ты получишь только счета, без всякой возни. Красота!
Но есть, конечно, и ложка дёгтя. Главный косяк TPH — таблица быстро превращается в ёлку с игрушками. Для каждого наследника добавляются его уникальные поля, и они автоматически nullable. Если наследников много и у каждого по 5-10 своих полей — таблица становится широкой, как Сибирь, и половина её будет забита NULL'ами. Зато JOIN'ов не нужно — все данные в одном месте. Выбор между производительностью на чтение и эффективностью хранения.
Короче, дискриминатор — это не страшно. Это просто метка, которая помогает EF Core не запутаться в собственном огороде из сущностей, когда они все живут в одной таблице.