Вступление
В этой статье мы углубимся в сопоставление наследования с помощью JPA и Hibernate в Java .
Java Persistence API (JPA) - это стандарт устойчивости экосистемы
Java. Это позволяет нам сопоставить нашу модель предметной области
непосредственно со структурой базы данных, а затем дает нам гибкость,
позволяющую манипулировать только объектами в нашем коде. Это позволяет
нам не возиться с громоздкими компонентами JDBC, такими как Connection
, ResultSet
и т. Д.
Мы составим исчерпывающее руководство по использованию JPA с Hibernate в качестве поставщика. В этой статье мы рассмотрим отображение наследования в Hibernate.
- Руководство по JPA с Hibernate: базовое сопоставление
- Руководство по JPA с Hibernate: отображение отношений
- Руководство по JPA с Hibernate: Сопоставление наследования ( вы здесь! )
- Руководство по JPA с Hibernate: запросы ( скоро! )
Сопоставление наследования
Базовое сопоставление, такое как сопоставление полей объекта или сопоставление отношений, где мы сопоставляем отношения между различными таблицами, очень распространено, и вы будете использовать эти методы практически в каждом приложении, которое вы создаете. Чуть реже вы будете отображать иерархии классов.
Идея здесь состоит в том, чтобы обрабатывать отображение иерархий классов. JPA предлагает несколько стратегий для достижения этой цели, и мы рассмотрим каждую из них:
- Сопоставленный суперкласс
- Один стол
- Одна таблица на (бетонный) класс
- Присоединенный стол
Модель домена
Прежде всего, давайте добавим наследование в нашу модель предметной области:
{.ezlazyload}
Как мы видим, мы представили Person
, который является суперклассом
как для Teacher
и для Student
и содержит имена и дату рождения, а
также адрес и пол.
В дополнение к этому мы добавили Vehicle
для управления транспортными
средствами учителей для управления парковкой.
Это может быть Car
или Motorcycle
. У каждого транспортного средства
есть номерной знак, но автомобиль может работать на сжиженном нефтяном
газе (что запрещено на определенных уровнях парковки), а мотоциклы могут
иметь коляску (для которой требуется место для парковки автомобиля).
Отображенный суперкласс
Начнем с простого - подхода с отображением суперкласса. Сопоставленный суперкласс - это класс, который не является сущностью, а содержит сопоставления. Это тот же принцип, что и встроенные классы, но применяется к наследованию.
Итак, предположим, что мы хотим сопоставить наши новые классы для
обработки парковки учителей в школе, мы сначала определим Vehicle
,
аннотированный с помощью @MappedSuperclass
:
@MappedSuperclass
public class Vehicle {
@Id
private String licensePlate;
}
Он содержит только идентификатор с @Id
, который является номерным
знаком транспортного средства.
Теперь мы хотим сопоставить наши две сущности: Car
и Motorcycle
.
Оба будут расширены от Vehicle
и унаследуют licensePlate
:
@Entity
class Car extends Vehicle {
private boolean runOnLpg;
}
@Entity
class Motorcycle extends Vehicle {
private boolean hasSideCar;
}
Хорошо, теперь мы определили сущности, и они наследуются от Vehicle
.
Но что происходит на стороне базы данных? JPA генерирует эти определения
таблиц:
create table Car (licensePlate varchar(255) not null, runOnLpg boolean not null, primary key (licensePlate))
create table Motorcycle (licensePlate varchar(255) not null, hasSideCar boolean not null, primary key (licensePlate))
У каждой сущности есть своя таблица со licensePlate
, который также
является первичным ключом этих таблиц. Таблицы Vehicle
нет .
@MappedSuperclass
не является сущностью. Фактически, к классу не могут
быть @Entity
аннотации @Entity и @MappedSuperclass
Каковы последствия того, что Vehicle
не является юридическим лицом?
Что ж, мы не можем искать Vehicle
с помощью EntityManager
.
Добавим несколько машин и мотоцикл:
insert into CAR(LICENSEPLATE, RUNONLPG) values('1 - ABC - 123', '1');
insert into CAR(LICENSEPLATE, RUNONLPG) values('2 - BCD - 234', '0');
insert into MOTORCYCLE(LICENSEPLATE, HASSIDECAR) values('M - ABC - 123', '0');
Интуитивно вы можете найти Vehicle
с номерным знаком 1 - ABC - 123
:
assertThrows(Exception.class, () -> entityManager.find(Vehicle.class, "1 - ABC - 123"));
И это вызовет исключение. Нет сохраненных сущностей " Vehicle
Однако
есть сохраняющиеся сущности Car
Давайте поищем Car
с этим номерным
знаком:
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
Стратегия за одним столом
Теперь перейдем к стратегии одного стола . Эта стратегия позволяет нам отображать все сущности иерархии классов в одну и ту же таблицу базы данных.
Если мы повторно используем наш пример с парковкой, это будет означать,
что все автомобили и мотоциклы будут сохранены в таблице VEHICLE
Чтобы настроить эту стратегию, нам понадобится несколько новых аннотаций, которые помогут нам определить эту взаимосвязь:
@Inheritance
- определяет стратегию наследования и используется для всех стратегий, кроме отображенных суперклассов.@DiscriminatorColumn
- определяет столбец, целью которого будет определение сущности, сохраненной в данной строке базы данных. Мы обозначим это какTYPE
, обозначая тип транспортного средства.@DiscriminatorValue
- который определяет значение столбца дискриминатора для данной сущности - то есть, является ли данная сущностьCar
илиMotorcycle
.
На этот раз Vehicle
- это управляемый JPA @Entity
, поскольку мы
сохраняем его в таблице. Также добавим к @DiscriminatorColumn
аннотации @Inheritance
и @DiscriminatorColumn:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "TYPE")
public class Vehicle {
@Id
private String licensePlate;
}
Аннотация @Inheritance
strategy
, который мы установили в
InheritanceType.SINGLE_TABLE
. Это позволяет JPA знать, что мы выбрали
подход единой таблицы. Этот тип также является типом по умолчанию,
поэтому, даже если бы мы не указали никаких стратегий, он все равно
будет SINGLE_TABLE
.
Мы также устанавливаем имя столбца дискриминатора как TYPE
(по
умолчанию DTYPE
). Теперь, когда JPA генерирует таблицы, это будет
выглядеть так:
create table Vehicle (TYPE varchar(31) not null, licensePlate varchar(255) not null, hasSideCar boolean, runOnLpg boolean, primary key (licensePlate))
Это имеет несколько последствий:
- Поля для
Car
иMotorcycle
хранятся в одной таблице , что может стать беспорядочным, если у нас много полей. - Все поля подкласса должны допускать значение NULL (потому что
Car
не может иметь значений дляMotorcycle
, и наоборот), что означает меньшую проверку на уровне базы данных.
При этом давайте теперь сопоставим наши Car
и Motorcycle
:
@Entity
@DiscriminatorValue("C")
class Car extends Vehicle {
private boolean runOnLpg;
}
@Entity
@DiscriminatorValue("M")
class Motorcycle extends Vehicle {
private boolean hasSideCar;
}
Здесь мы определяем значения столбца дискриминатора для наших сущностей.
Мы выбрали C
для автомобилей и M
для мотоциклов. По умолчанию JPA
использует имя сущностей. В нашем случае Car
и Motorcycle
соответственно.
Теперь давайте добавим несколько транспортных средств и посмотрим, как с
ними работает EntityManager
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('1 - ABC - 123', 'C', '1', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('2 - BCD - 234', 'C', '0', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('M - ABC - 123', 'M', null, '0');
С одной стороны, мы можем получить каждую сущность Car
или
Motorcycle
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
Но поскольку Vehicle
также является сущностью, мы также можем получать
сущности в качестве их суперкласса - Vehicle
:
Vehicle foundCar = entityManager.find(Vehicle.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
Фактически, мы даже можем сохранить объект Vehicle
который не является
ни Car
ни Motorcycle
:
Vehicle vehicle = new Vehicle();
vehicle.setLicensePlate("T - ABC - 123");
entityManager.persist(vehicle);
Что переводится в следующий SQL-запрос:
insert into Vehicle (TYPE, licensePlate) values ('Vehicle', ?)
Хотя мы, возможно, не хотим, чтобы это произошло - мы должны
использовать аннотацию @Entity
Vehicle
с этой стратегией.
Если вы хотите отключить эту функцию, простой вариант - сделать класс
Vehicle
abstract
, не позволяя никому создавать его экземпляры. Если
он не является инстанцируемым, его нельзя сохранить как объект, даже
если он аннотирован как один.
Одна таблица на класс.
Следующая стратегия называется One Table Per Class , которая, как следует из названия, создает одну таблицу для каждого класса в иерархии .
Хотя вместо этого мы могли бы использовать термин «конкретный класс» , поскольку он не создает таблицы для абстрактных классов.
Этот подход очень похож на подход Mapped Superclass с той лишь разницей, что суперкласс также является сущностью .
Чтобы JPA знало, что мы хотели бы применить эту стратегию, мы установим
InheritanceType
на TABLE_PER_CLASS
в нашей аннотации @Inheritance
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
@Id
private String licensePlate;
}
Наши Car
и Motorcycle
просто нужно сопоставить с помощью @Entity
и
все готово. Определения таблиц такие же, как и для отображаемого
суперкласса, плюс VEHICLE
(потому что это конкретный класс).
Но то, что отличается от сопоставленного суперкласса, заключается в том,
что мы можем искать Vehicle
, а также сущность Car
или Motorcycle
Vehicle foundVehicle = entityManager.find(Vehicle.class, "1 - ABC - 123");
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundVehicle).isNotNull();
assertThat(foundVehicle.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
Стратегия объединенного стола
Наконец, есть стратегия объединенного стола. Он создает одну таблицу для каждой сущности и сохраняет каждый столбец там, где он естественным образом.
Возьмем нашу иерархию Person
/ Student
/ Teacher
Если мы реализуем
его с использованием стратегии объединенных таблиц, мы получим три
таблицы:
create table Person (id bigint not null, city varchar(255), number varchar(255), street varchar(255), birthDate date, FIRST_NAME varchar(255), gender varchar(255), lastName varchar(255), primary key (id))
create table STUD (wantsNewsletter boolean not null, id bigint not null, primary key (id))
create table Teacher (id bigint not null, primary key (id))
Первый, PERSON
, получает столбцы для всех полей в Person
, в то
время как другие получают столбцы только для своих собственных полей, а
также id
который связывает таблицы вместе.
При поиске студента, JPA будет выдавать запрос SQL с соединением между
STUD
и PERSON
таблицами для того , чтобы получить все данные
студента.
Чтобы сопоставить эту иерархию, мы будем использовать стратегию
InheritanceType.JOINED
в аннотации @Inheritance
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
@Id
@GeneratedValue
private Long id;
private String lastName;
@Column(name = "FIRST_NAME")
private String firstName;
private LocalDate birthDate;
@Enumerated(EnumType.STRING)
private Student.Gender gender;
@Embedded
private Address address;
}
Другие наши объекты просто отображаются с помощью @Entity
:
@Entity
public class Student extends Person {
@Id
private Long id;
private boolean wantsNewsletter;
private Gender gender;
}
А также:
@Entity
public class Teacher extends Person {
@Id
private Long id;
Давайте также определим ENUM, который мы использовали в классе Student
enum GENDER {
MALE, FEMALE
}
Итак, мы можем получить Person
, Student
и Teacher
а также
сохранить их с помощью EntityManager.persist()
.
Опять же, если мы не хотим создавать Person
мы должны сделать их
abstract
.
Заключение
В этой статье мы углубились в отображение наследования с использованием JPA и Hibernate и рассмотрели несколько различных ситуаций, с которыми вы можете столкнуться.
Код для этой серии можно найти на GitHub .