Руководство по JPA с Hibernate - Сопоставление наследования

Введение В этой статье мы углубимся в отображение наследования с помощью JPA и Hibernate в Java. Java Persistence API (JPA) - это стандарт устойчивости экосистемы Java. Это позволяет нам сопоставить нашу модель предметной области непосредственно со структурой базы данных, а затем дает нам гибкость, позволяющую манипулировать только объектами в нашем коде. Это позволяет нам не возиться с громоздкими компонентами JDBC, такими как Connection, ResultSet и т. Д. Мы составим исчерпывающее руководство по использованию JPA с Hibernate в качестве его

Вступление

В этой статье мы углубимся в сопоставление наследования с помощью JPA и Hibernate в Java .

Java Persistence API (JPA) - это стандарт устойчивости экосистемы Java. Это позволяет нам сопоставить нашу модель предметной области непосредственно со структурой базы данных, а затем дает нам гибкость, позволяющую манипулировать только объектами в нашем коде. Это позволяет нам не возиться с громоздкими компонентами JDBC, такими как Connection , ResultSet и т. Д.

Мы составим исчерпывающее руководство по использованию JPA с Hibernate в качестве поставщика. В этой статье мы рассмотрим отображение наследования в Hibernate.

Сопоставление наследования

Базовое сопоставление, такое как сопоставление полей объекта или сопоставление отношений, где мы сопоставляем отношения между различными таблицами, очень распространено, и вы будете использовать эти методы практически в каждом приложении, которое вы создаете. Чуть реже вы будете отображать иерархии классов.

Идея здесь состоит в том, чтобы обрабатывать отображение иерархий классов. JPA предлагает несколько стратегий для достижения этой цели, и мы рассмотрим каждую из них:

  • Сопоставленный суперкласс
  • Один стол
  • Одна таблица на (бетонный) класс
  • Присоединенный стол

Модель домена

Прежде всего, давайте добавим наследование в нашу модель предметной области:

домен-1{.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 .

comments powered by Disqus