Hibernate Inheritance Mapping

Hibernate Inheritance Mapping

1. обзор

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

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

  • MappedSuperclass - родительские классы, не могут быть сущностями

  • Одна таблица - объекты из разных классов с общим предком помещаются в одну таблицу

  • Объединенная таблица - каждый класс имеет свою таблицу, и запрос сущности подкласса требует объединения таблиц

  • Table-Per-Class - все свойства класса находятся в его таблице, поэтому соединение не требуется

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

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

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

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

2. MappedSuperclassс

При использовании стратегииMappedSuperclass наследование очевидно только в классе, но не в модели сущности.

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

@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // constructor, getters, setters
}

Notice that this class no longer has an @Entity annotation, так как сам по себе он не сохраняется в базе данных.

Затем давайте добавим подклассEmployee:

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

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

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

3. Одноместный стол

The Single Table strategy creates one table for each class hierarchy. Это также стратегия по умолчанию, выбранная 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 needs a way to differentiate between them.

By default, this is done through a discriminator column called DTYPE, в котором имя объекта является значением.

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

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

Здесь мы решили различать сущности подклассаMyProduct по столбцуinteger под названием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(“not null”) - любая строка со значением дискриминатора, не совпадающим ни с одним из связанных с определениями сущностей, будет сопоставлена ​​с классом с этой аннотацией

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

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

This strategy has the advantage of polymorphic query performance since only one table needs to be accessed when querying parent entities. С другой стороны, это также означает, что свойства объектаwe can no longer use NOT NULL constraints on sub-class.

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

Using this strategy, each class in the hierarchy is mapped to its table. Единственный столбец, который постоянно появляется во всех таблицах, - это идентификатор, который будет использоваться для их объединения при необходимости.

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

@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 {
    // ...
}

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, что может привести к снижению производительности для большого количества записей.

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

5. Таблица на класс

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

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

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

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

    private String manufacturer;

    // standard constructor, getters, setters
}

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

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

The use of UNION can also lead to inferior performance when choosing this strategy. Другая проблема заключается в том, что мы больше не можем использовать генерацию идентификационного ключа.

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", "example");
    session.save(emp);

    assertThat(session.createQuery(
      "from com.example.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.

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