Какие типы наследования поддерживает JPA
JPA поддерживает три стратегии наследования сущностей, каждая из которых по-разному маппит иерархию классов на таблицы базы данных. Выбор стратегии влияет на производительность,...
Обзор
JPA поддерживает три стратегии наследования сущностей, каждая из которых по-разному маппит иерархию классов на таблицы базы данных. Выбор стратегии влияет на производительность, нормализацию и сложность запросов.
🟢 Junior Level
3 стратегии наследования
JPA поддерживает 3 стратегии наследования:
- SINGLE_TABLE — все классы в одной таблице
- JOINED — каждый класс в своей таблице с JOIN
- TABLE_PER_CLASS — каждый класс в своей таблице (без JOIN)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public class Payment {
@Id
private Long id;
private BigDecimal amount;
}
@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
private String cardNumber;
}
@Entity
@DiscriminatorValue("CASH")
public class CashPayment extends Payment {
private LocalDate cashDate;
}
🟡 Middle Level
SINGLE_TABLE
Таблица payments:
id | type | amount | card_number | cash_date
1 | CARD | 100 | 1234 | null
2 | CASH | 50 | null | 2024-01-01
✅ Быстро (нет JOIN)
✅ Простые запросы
❌ Много NULL для подклассов
❌ Нельзя добавить NOT NULL constraint
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
public abstract class Payment { }
@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment { }
JOINED
Таблица payments:
id | amount
1 | 100
2 | 50
Таблица card_payments:
payment_id | card_number
1 | 1234
Таблица cash_payments:
payment_id | cash_date
2 | 2024-01-01
✅ Нормализовано
✅ NOT NULL constraints возможны
❌ JOIN для каждого запроса
❌ Медленнее при выборке
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment { }
@Entity
@PrimaryKeyJoinColumn(name = "payment_id")
public class CardPayment extends Payment { }
TABLE_PER_CLASS
Таблица card_payments:
id | amount | card_number
1 | 100 | 1234
Таблица cash_payments:
id | amount | cash_date
2 | 50 | 2024-01-01
✅ Нет NULL, нет JOIN для конкретного типа
❌ UNION для запроса родителя
❌ Сложные запросы к родителю
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment { }
@Entity
public class CardPayment extends Payment { }
TABLE_PER_CLASS — optional в JPA spec. Не все провайдеры поддерживают. Hibernate поддерживает, но для portability проверяйте ваш провайдер.
Типичные ошибки
// ❌ SINGLE_TABLE с многими подклассами
// Много полей, много NULL → сложно поддерживать
// ❌ JOINED для high-performance
// JOIN для каждого запроса → медленно
// ❌ TABLE_PER_CLASS с полиморфными запросами
// UNION для SELECT FROM Payment → медленно
🔴 Senior Level
Сравнение стратегий
| Стратегия | INSERT | SELECT (тип) | SELECT (родитель) | NULL | JOIN | Полимофизм |
|---|---|---|---|---|---|---|
| SINGLE_TABLE | Быстро | Быстро | Быстро | Много | Нет | ✅ |
| JOINED | Медленно | Медленно | Медленно | Нет | Да | ✅ |
| TABLE_PER_CLASS | Быстро | Быстро (для конкретного подкласса). Медленно (UNION ALL при запросе родителя) | UNION | Нет | Нет | ⚠️ |
Когда что использовать
SINGLE_TABLE:
✅ Маленькая иерархия (2-3 подкласса)
✅ Поля подклассов могут быть NULL
✅ Высокая производительность нужна
✅ Полиморфные запросы часты
JOINED:
✅ Нормализация важна
✅ Поля подклассов NOT NULL
✅ Редкие полиморфные запросы
✅ Разные подклассы имеют много полей
TABLE_PER_CLASS:
✅ Каждый подкласс — независимая сущность
✅ Полиморфные запросы редки
✅ Нет общих полей у подклассов
Полиморфные запросы
// SINGLE_TABLE — один запрос
SELECT p FROM Payment p // SELECT * FROM payments
// JOINED — JOIN всех таблиц
SELECT p FROM Payment p // SELECT * FROM payments p
// LEFT JOIN card_payments cp ON ...
// LEFT JOIN cash_payments cash ON ...
// TABLE_PER_CLASS — UNION
SELECT p FROM Payment p // SELECT * FROM card_payments
// UNION ALL
// SELECT * FROM cash_payments
TYPE и TREAT в JPQL
// TYPE — фильтр по типу
@Query("SELECT p FROM Payment p WHERE TYPE(p) = CardPayment")
List<CardPayment> findCardPayments();
// TREAT — cast к подклассу
@Query("SELECT p FROM Payment p JOIN TREAT(p AS CardPayment) cp WHERE cp.cardNumber = :number")
List<Payment> findByCardNumber(@Param("number") String number);
Hibernate 6 улучшения
Hibernate 6:
- Улучшенная обработка TABLE_PER_CLASS
- Лучшая оптимизация JOIN для JOINED
- Автоматический discriminator для SINGLE_TABLE
Продвинутые паттерны
// Pattern: @MappedSuperclass для общих полей
@MappedSuperclass
public abstract class BaseEntity {
@Id
private Long id;
private LocalDateTime createdAt;
}
@Entity
public class CardPayment extends BaseEntity { }
@Entity
public class CashPayment extends BaseEntity { }
// Каждая сущность — отдельная таблица
// Нет полиморфных запросов
@MappedSuperclass — НЕ entity: нельзя делать запросы к нему, нет таблицы, нельзя использовать в полиморфных запросах. Это просто шаблон для общих полей. Классический interview trap: “чем @MappedSuperclass отличается от @Entity?”
Best Practices
✅ SINGLE_TABLE для простых случаев (2-3 подкласса)
✅ JOINED для нормализации и NOT NULL
✅ TABLE_PER_CLASS для независимых подклассов
✅ Избегать глубокой иерархии (>3 уровней). Причина: глубокие иерархии усложняют запросы (больше JOIN в JOINED, больше колонок в SINGLE_TABLE, больше UNION веток в TABLE_PER_CLASS), затрудняют понимание доменной модели и создают каскадные проблемы при изменениях.
✅ @MappedSuperclass когда не нужен полиморфизм
✅ TYPE/TREAT для типизированных запросов
❌ SINGLE_TABLE с многими подклассами
❌ JOINED для high-performance систем
❌ TABLE_PER_CLASS с частыми полиморфными запросами
❌ Глубокие иерархии наследования
🎯 Шпаргалка для интервью
Обязательно знать:
- 3 стратегии: SINGLE_TABLE (одна таблица), JOINED (таблица на класс), TABLE_PER_CLASS (таблица на класс, UNION для родителя)
- SINGLE_TABLE — быстро (нет JOIN), но много NULL, нельзя NOT NULL constraint
- JOINED — нормализовано, NOT NULL возможны, но JOIN для каждого запроса — медленно
- TABLE_PER_CLASS — нет NULL/JOIN для конкретного типа, но UNION для полиморфных запросов
- @MappedSuperclass — НЕ entity, нет таблицы, нельзя полиморфные запросы — шаблон для общих полей
- TYPE для фильтра по типу, TREAT для cast к подклассу в JPQL
Частые уточняющие вопросы:
- Какую стратегию выбрать? SINGLE_TABLE для 2-3 подклассов и скорости; JOINED для нормализации; TABLE_PER_CLASS для независимых подклассов
- Почему глубокая иерархия плоха? Больше JOIN (JOINED), больше колонок (SINGLE_TABLE), больше UNION (TABLE_PER_CLASS)
- @MappedSuperclass vs @Entity с наследованием? @MappedSuperclass — нет полиморфных запросов, нет таблицы; @Entity — есть
- TABLE_PER_CLASS optional в JPA spec? Да — не все провайдеры поддерживают, Hibernate поддерживает
Красные флаги (НЕ говорить):
- «SINGLE_TABLE с 10 подклассами» — много NULL, сложно поддерживать
- «JOINED для high-performance системы» — JOIN для каждого запроса, медленно
- «TABLE_PER_CLASS с частыми полиморфными запросами» — UNION ALL, ещё медленнее
- «@MappedSuperclass для полиморфных запросов» — нельзя делать запросы к @MappedSuperclass
Связанные темы:
- [[26. Что такое JPQL и чем он отличается от SQL]]
- [[27. Что такое Criteria API и когда его использовать]]
- [[23. Как правильно использовать @OneToMany и @ManyToOne]]
- [[24. В чём особенности bidirectional relationships]]