Что такое аннотация @Inheritance и какие стратегии наследования поддерживает Hibernate (SINGLE_TABLE, TABLE_PER_CLASS, JOINED)?
@Inheritance задаёт стратегию хранения иерархии классов в БД. SINGLE_TABLE — одна таблица с discriminator-колонкой, TABLE_PER_CLASS — отдельная таблица для каждого конкретного класса, JOINED — базовая таблица + таблица для каждого подкласса с FK.
Аннотация @Inheritance
Аннотация @Inheritance из пакета javax.persistence (или jakarta.persistence) применяется к корневой сущности иерархии и указывает Hibernate, как хранить полиморфные данные в реляционной БД. Все три стратегии — это компромисс между нормализацией, производительностью и простотой запросов.
SINGLE_TABLE
Все классы иерархии хранятся в одной таблице. Hibernate добавляет дополнительную колонку-дискриминатор, по которой определяет тип сущности. Это самая производительная стратегия для полиморфных запросов, но нарушает нормализацию.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "vehicle_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Vehicle {
@Id @GeneratedValue
private Long id;
private String brand;
}
@Entity
@DiscriminatorValue("CAR")
public class Car extends Vehicle {
private int doors;
}
@Entity
@DiscriminatorValue("TRUCK")
public class Truck extends Vehicle {
private double payload;
}
Результат: одна таблица Vehicle со столбцами id, brand, vehicle_type, doors, payload. Столбцы подклассов могут быть NULL для других типов.
Плюсы: нет JOIN, быстрые полиморфные запросы, простая схема.
Минусы: NOT NULL-ограничения на поля подклассов невозможны, таблица растёт при добавлении подклассов.
TABLE_PER_CLASS
Каждый конкретный класс получает собственную таблицу со всеми полями, включая унаследованные. Абстрактный базовый класс таблицы не создаёт.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
@Id @GeneratedValue(strategy = GenerationType.TABLE) // SEQUENCE не работает
private Long id;
private BigDecimal amount;
}
@Entity
public class CreditCardPayment extends Payment {
private String cardNumber;
}
@Entity
public class BankTransferPayment extends Payment {
private String iban;
}
Таблицы: CreditCardPayment(id, amount, cardNumber) и BankTransferPayment(id, amount, iban).
Плюсы: нет NULL-столбцов, возможны NOT NULL-ограничения.
Минусы: полиморфный запрос SELECT * FROM Payment транслируется в UNION ALL по всем таблицам. Стратегии генерации ID через IDENTITY/SEQUENCE несовместимы — нужно TABLE или SEQUENCE с allocationSize.
JOINED
Базовый класс хранится в отдельной таблице, каждый подкласс — в своей таблице с FK на базовую. Наиболее нормализованный подход.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Employee {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
}
@Entity
@PrimaryKeyJoinColumn(name = "employee_id")
public class Manager extends Employee {
private String department;
}
@Entity
@PrimaryKeyJoinColumn(name = "employee_id")
public class Developer extends Employee {
private String primaryLanguage;
}
Таблицы: Employee(id, name, email), Manager(employee_id, department), Developer(employee_id, primaryLanguage).
Плюсы: нормализация, возможны FK и NOT NULL, минимальное дублирование.
Минусы: каждый запрос требует JOIN, что замедляет выборку на глубоких иерархиях.
Сравнительная таблица
- SINGLE_TABLE — выбирайте при быстром чтении и небольшом числе подклассов с похожими полями.
- TABLE_PER_CLASS — редко используется, подходит когда полиморфные запросы не нужны.
- JOINED — выбирайте при сложной иерархии, необходимости DB-ограничений и когда нормализация важнее скорости.
Подводные камни
- В SINGLE_TABLE нельзя поставить
@Column(nullable = false)на поля подклассов — они всегда NULL для других типов. - TABLE_PER_CLASS несовместим со стратегиями генерации IDENTITY и SEQUENCE с общим счётчиком — возникают дубликаты ID.
- JOINED добавляет JOIN для каждого уровня иерархии — трёхуровневая иерархия = два JOIN на каждый SELECT.
- В SINGLE_TABLE нельзя использовать
@OneToManyсmappedByот подкласса к другой сущности без осторожности — discriminator не фильтруется автоматически в некоторых версиях Hibernate. - Смешивание стратегий в одной иерархии невозможно — все подклассы наследуют стратегию корневого класса.
- При использовании JOINED и Criteria API полиморфные запросы автоматически генерируют LEFT OUTER JOIN — это может быть медленнее INNER JOIN при большом числе строк.
- TABLE_PER_CLASS и
@ManyToOneна базовый абстрактный тип генерируют UNION ALL — крайне неэффективно на больших таблицах.
Common mistakes
- Путать термин «inheritance strategies» с соседним механизмом Hibernate.
- Не называть границу lifecycle, transaction, thread или request для «inheritance strategies».
- Игнорировать production-эффекты «inheritance strategies»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «inheritance strategies» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «inheritance strategies».
- Уточнить, какие настройки или API меняют «inheritance strategies» в реальном сервисе.