HibernateMiddleSystem design

Что такое аннотация @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» в реальном сервисе.

Sources

Related topics