Вопрос 30 · Раздел 16

Какие типы наследования поддерживает JPA

JPA поддерживает три стратегии наследования сущностей, каждая из которых по-разному маппит иерархию классов на таблицы базы данных. Выбор стратегии влияет на производительность,...

Версии по языкам: English Russian Ukrainian

Обзор

JPA поддерживает три стратегии наследования сущностей, каждая из которых по-разному маппит иерархию классов на таблицы базы данных. Выбор стратегии влияет на производительность, нормализацию и сложность запросов.


🟢 Junior Level

3 стратегии наследования

JPA поддерживает 3 стратегии наследования:

  1. SINGLE_TABLE — все классы в одной таблице
  2. JOINED — каждый класс в своей таблице с JOIN
  3. 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]]