Наследование сущностей с помощью Single Table (пример на Hibernate и Spring Boot)

Продолжим видоизменять предыдущий пример с заказчиками, только теперь наследовать сущности будем другим способом: не с помощью Joined Table (как там), а с помощью Single Table. Как понятно из названия, все заказчики теперь будут храниться в одной таблице.
Этот способ самый эффективный с точки зрения производительности, так как с одной таблицей работать быстрее, чем с несколькими.

Итак, напомню модель.

Модель

Есть два класса заказчиков — внешние и внутренние, все они наследники Customer:

@Entity
@Inheritance
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;

// getters/setters/constructors
}

Внешний заказчик ExternalCustomer:

@Entity
public class ExternalCustomer extends Customer {
   private long sum;

// getters/setters/constructors
}

Внутренний заказчик EmpoyeeCustomer:

@Entity
public class EmployeeCustomer extends Customer {
    private int monthsInCompany;

// getters/setters/constructors
}

@Inheritance — выбор стратегии наследования

Стратегия наследования указывается в аннотации @Inheritance родительского класса Customer.

У нас это Single Table, потому что InheritanceType.SINGLE_TABLE является стратегией по умолчанию. Но можно было бы в аннотации явно указать:

@Inheritance(
        strategy = InheritanceType.SINGLE_TABLE
)

Результат был бы тот же.

Схема в базе данных

В итоге генерируется такая схема, состоящая из одной таблицы:

Single Table для всех заказчиков
Single Table для всех заказчиков

Как видите, все заказчики хранятся в одной таблице customer. Но как же подклассы различаются? С помощью столбца-дискриминатора.

Столбец-дискриминатор DTYPE

Обратите внимание, что при генерации схемы Hibernate добавляет столбец DTYPE — это столбец-дискриминатор. Он показывает, к какому классу принадлежит заказчик: ExternalCustomer или EmployeeCustomer.  Значением в этом столбце по умолчанию будет название сущности: EmployeeCustomer или ExternalCustomer.

Добавление заказчиков

Создадим двух заказчиков и сохраним их в базе:

EmployeeCustomer employeeCustomer = new EmployeeCustomer();
employeeCustomer.setMonthsInCompany(10);
employeeCustomer.setName("Petr");
customerRepository.save(employeeCustomer);

ExternalCustomer externalCustomer = new ExternalCustomer();
externalCustomer.setSum(110);
externalCustomer.setName("Vasya");
customerRepository.save(externalCustomer);
Результат в базе
Результат в базе

В консоли видно, что генерируется два оператора insert:

Hibernate: insert into customer (name, months_in_company, dtype, id) values (?, ?, 'EmployeeCustomer', ?)
Hibernate: insert into customer (name, sum, dtype, id) values (?, ?, 'ExternalCustomer', ?)

Тогда как в предыдущем примере было четыре оператора insert — еще заполнялась общая таблица, что гораздо менее эффективно:

Hibernate: insert into customer (name, id) values (?, ?)
Hibernate: insert into employee_customer (months_in_company, id) values (?, ?)
Hibernate: insert into customer (name, id) values (?, ?)
Hibernate: insert into external_customer (sum, id) values (?, ?)

Недостаток — запрет @NotNull

Как видно на схеме выше, в таблице заказчиков customer есть как общие для всех заказчиков столбцы — поля класса Customer, так и столбцы подклассов ExternalCustomer и EmployeeCustomer. Столбцы подклассов не всегда заполнены: если заказчик ExternalCustomer, то столбцы, зарезервированные под EmployeeCustomer, остаются пустыми. И наоборот.

Поэтому невозможно ограничить значение столбцов наследников ограничением @NotNull.

Если NotNull constraint непременно нужен, то надо использовать другую стратегию, например Joined Table.

Поиск заказчиков

Поиск полиморфичен, то есть мы в HQL выбираем из класса Customer, но полученные заказчики имеют конкретный тип EmployeeCustomer либо ExternalCustomer:

List<Customer> customers = customerRepository.findAll();
Выше приведен поиск методом findAll() JpaRepository, но под капотом там HQL.

При этом генерируется SQL SELECT из одной таблицы customer (других нет):

select customer0_.id as id2_0_, customer0_.name as name3_0_, customer0_.months_in_company as months_i4_0_, customer0_.sum as sum5_0_, customer0_.dtype as dtype1_0_ 
from customer customer0_

Тогда как в предыдущем примере был SELECT с JOIN, что менее эффективно.

Шаблон проектирования Strategy

Как уже говорилось в предыдущем примере Joined Table, наследование сущностей нужно прежде всего не для сохранения общей информации в родительском классе (для этого пригодна композиция), а для того, чтобы сущности было удобно использовать в шаблонах проектирования. Смена стратегии наследования на Single Table не испортила написанный на основе  Strategy код вычисления скидки заказчика — он по-прежнему работает, исходный код доступен.

Итоги

Стратегия наследования Single Table — самая простая и эффективная. Единственный недостаток — невозможность использовать ограничение NotNull для столбцов подклассов.

Исходный код примера есть на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *