Перейти к основному содержимому

Отображение наследования в спящем режиме

· 7 мин. чтения

1. Обзор

В реляционных базах данных нет простого способа отображать иерархии классов в таблицы базы данных.

Для решения этой проблемы в спецификации JPA предусмотрено несколько стратегий:

  • MappedSuperclass — родительские классы, не могут быть сущностями .
  • Единая таблица — объекты из разных классов с общим предком помещаются в одну таблицу.
  • Объединенная таблица — у каждого класса есть своя таблица, и для запроса сущности подкласса требуется объединение таблиц.
  • Таблица для каждого класса — все свойства класса находятся в его таблице, поэтому объединение не требуется.

Каждая стратегия приводит к различной структуре базы данных.

Наследование сущностей означает, что мы можем использовать полиморфные запросы для получения всех сущностей подкласса при запросе суперкласса.

Поскольку Hibernate является реализацией JPA, он содержит все вышеперечисленное, а также несколько специфичных для Hibernate функций, связанных с наследованием.

В следующих разделах мы рассмотрим доступные стратегии более подробно.

2. MappedSuperclass

При использовании стратегии MappedSuperclass наследование проявляется только в классе, но не в модели объекта.

Начнем с создания класса Person , который будет представлять родительский класс:

@MappedSuperclass
public class Person {

@Id
private long personId;
private String name;

// constructor, getters, setters
}

Обратите внимание, что у этого класса больше нет аннотации @Entity , так как он не будет сохраняться в базе данных сам по себе.

Далее добавим подкласс Employee :

@Entity
public class MyEmployee extends Person {
private String company;
// constructor, getters, setters
}

В базе данных это будет соответствовать одной таблице MyEmployee с тремя столбцами для объявленных и унаследованных полей подкласса.

Если мы используем эту стратегию, предки не могут содержать ассоциации с другими сущностями.

3. Один стол

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

Мы можем определить стратегию, которую хотим использовать, добавив аннотацию @Inheritance к суперклассу:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
@Id
private long productId;
private String name;

// constructor, getters, setters
}

Идентификатор сущностей также определяется в суперклассе.

Затем мы можем добавить сущности подкласса:

@Entity
public class Book extends MyProduct {
private String author;
}
@Entity
public class Pen extends MyProduct {
private String color;
}

3.1. Значения дискриминатора

Поскольку записи для всех сущностей будут в одной таблице, Hibernate нужен способ различать их.

По умолчанию это делается с помощью столбца дискриминатора с именем DTYPE , в котором в качестве значения указано имя объекта.

Чтобы настроить столбец дискриминатора, мы можем использовать аннотацию @DiscriminatorColumn :

@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type",
discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
// ...
}

Здесь мы решили различать сущности подкласса MyProduct по целочисленному столбцу с именем product_type .

Затем нам нужно сообщить Hibernate, какое значение будет иметь каждая запись подкласса для столбца product_type :

@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
// ...
}
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
// ...
}

Hibernate добавляет два других предопределенных значения, которые может принимать аннотация — null и not null :

  • @DiscriminatorValue("null") означает, что любая строка без значения дискриминатора будет сопоставлена с классом сущности с этой аннотацией; это можно применить к корневому классу иерархии.
  • @DiscriminatorValue («не нуль») — любая строка со значением дискриминатора, не совпадающим ни с одним из значений, связанных с определениями сущностей, будет сопоставлена с классом с этой аннотацией.

Вместо столбца мы также можем использовать специфичную для Hibernate аннотацию @DiscriminatorFormula для определения различающихся значений:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when author is not null then 1 else 2 end")
public class MyProduct { ... }

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

С другой стороны, это также означает, что мы больше не можем использовать ограничения NOT NULL для свойств сущности подкласса .

4. Присоединенный стол

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

Давайте создадим суперкласс, использующий эту стратегию:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
@Id
private long animalId;
private String species;

// constructor, getters, setters
}

Затем мы можем просто определить подкласс:

@Entity
public class Pet extends Animal {
private String name;

// constructor, getters, setters
}

Обе таблицы будут иметь столбец идентификатора animalId .

Первичный ключ объекта Pet также имеет ограничение внешнего ключа на первичный ключ его родительского объекта.

Чтобы настроить этот столбец, мы можем добавить аннотацию @PrimaryKeyJoinColumn :

@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
// ...
}

Недостатком этого метода сопоставления наследования является то, что для извлечения сущностей требуются соединения между таблицами , что может привести к снижению производительности при большом количестве записей.

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

5. Таблица для каждого класса

Стратегия «Таблица на класс» сопоставляет каждый объект с его таблицей, которая содержит все свойства объекта, включая унаследованные.

Результирующая схема аналогична схеме с использованием @MappedSuperclass. Но Table per Class действительно будет определять сущности для родительских классов, в результате позволяя ассоциации и полиморфные запросы.

Чтобы использовать эту стратегию, нам нужно всего лишь добавить аннотацию @Inheritance к базовому классу:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
@Id
private long vehicleId;

private String manufacturer;

// standard constructor, getters, setters
}

Затем мы можем создать подклассы стандартным способом.

Это ничем не отличается от простого сопоставления каждой сущности без наследования. Различие очевидно при запросе к базовому классу, который также возвращает все записи подкласса с помощью оператора UNION в фоновом режиме.

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

6. Полиморфные запросы

Как уже упоминалось, запрос к базовому классу также извлечет все объекты подкласса.

Давайте посмотрим на это поведение в действии с тестом JUnit:

@Test
public void givenSubclasses_whenQuerySuperclass_thenOk() {
Book book = new Book(1, "1984", "George Orwell");
session.save(book);
Pen pen = new Pen(2, "my pen", "blue");
session.save(pen);

assertThat(session.createQuery("from MyProduct")
.getResultList()).hasSize(2);
}

В этом примере мы создали два объекта Book и Pen , а затем запросили их суперкласс MyProduct , чтобы убедиться, что мы получим два объекта.

Hibernate также может запрашивать интерфейсы или базовые классы, которые не являются сущностями, но расширяются или реализуются классами сущностей.

Давайте посмотрим на тест JUnit, используя наш пример @MappedSuperclass :

@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
MyEmployee emp = new MyEmployee(1, "john", "foreach");
session.save(emp);

assertThat(session.createQuery(
"from com.foreach.hibernate.pojo.inheritance.Person")
.getResultList())
.hasSize(1);
}

Обратите внимание, что это также работает для любого суперкласса или интерфейса, будь то @MappedSuperclass или нет. Отличие от обычного запроса HQL заключается в том, что мы должны использовать полное имя, поскольку они не являются сущностями, управляемыми Hibernate.

Если мы не хотим, чтобы этот тип запроса возвращал подкласс, нам нужно всего лишь добавить аннотацию Hibernate @Polymorphism к его определению с типом EXPLICIT :

@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}

В этом случае при запросе Items записи Bag не будут возвращены.

7. Заключение

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

Полный исходный код примеров можно найти на GitHub .