Продолжим видоизменять предыдущий пример с заказчиками, только теперь наследовать сущности будем другим способом: не с помощью 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 )
Результат был бы тот же.
Схема в базе данных
В итоге генерируется такая схема, состоящая из одной таблицы:
Как видите, все заказчики хранятся в одной таблице 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();
При этом генерируется 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.