Обзор идентификаторов в Hibernate

Обзор идентификаторов в Hibernate

1. Вступление

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

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

2. Простые идентификаторы

Самый простой способ определить идентификатор - использовать аннотацию@Id.

Простые идентификаторы сопоставляются с помощью@Id одному свойству одного из следующих типов: примитивные типы Java и примитивные типы оболочки,String, Date, BigDecimal, BigInteger.

Давайте посмотрим на быстрый пример определения объекта с первичным ключом типаlong:

@Entity
public class Student {

    @Id
    private long studentId;

    // standard constructor, getters, setters
}

3. Сгенерированные идентификаторы

Если мы хотим, чтобы значение первичного ключа генерировалось автоматически для нас,we can add the @GeneratedValue annotation.

Это может использовать 4 типа генерации: AUTO, IDENTITY, SEQUENCE, TABLE.

Если мы не указываем значение явно, по умолчанию используется тип генерации АВТО.

3.1. AUTO Генерация

Если мы используем тип генерации по умолчанию, поставщик сохраняемости будет определять значения на основе типа атрибута первичного ключа. Этот тип может быть числовым илиUUID.

Для числовых значений генерация основана на генераторе последовательности или таблицы, тогда как значенияUUID будут использоватьUUIDGenerator.

Давайте посмотрим на пример сопоставления первичного ключа объекта с использованием стратегии генерации АВТО:

@Entity
public class Student {

    @Id
    @GeneratedValue
    private long studentId;

    // ...
}

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

An interesting feature introduced in Hibernate 5 is the UUIDGenerator. Чтобы использовать это, все, что нам нужно сделать, это объявить идентификатор типаUUID с аннотацией@GeneratedValue:

@Entity
public class Course {

    @Id
    @GeneratedValue
    private UUID courseId;

    // ...
}

Hibernate сгенерирует идентификатор вида «8dd5f315-9788-4d00-87bb-10eed9eff566».

3.2. IDENTITY Генерация

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

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

@Entity
public class Student {

    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private long studentId;

    // ...
}

Следует отметить, что генерация IDENTITY отключает пакетные обновления.

3.3. SEQUENCE Генерация

Чтобы использовать идентификатор на основе последовательности, Hibernate предоставляет классSequenceStyleGenerator.

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

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

@Entity
public class User {
    @Id
    @GeneratedValue(generator = "sequence-generator")
    @GenericGenerator(
      name = "sequence-generator",
      strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
      parameters = {
        @Parameter(name = "sequence_name", value = "user_sequence"),
        @Parameter(name = "initial_value", value = "4"),
        @Parameter(name = "increment_size", value = "1")
        }
    )
    private long userId;

    // ...
}

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

SEQUENCE - это тип генерации, рекомендованный документацией Hibernate.

The generated values are unique per sequence. Если вы не укажете имя последовательности, Hibernate будет повторно использовать один и тот жеhibernate_sequence для разных типов.

3.4. ТАБЛИЦА Генерация

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

Давайте настроим имя таблицы, используя аннотацию@TableGenerator:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
      generator = "table-generator")
    @TableGenerator(name = "table-generator",
      table = "dep_ids",
      pkColumnName = "seq_id",
      valueColumnName = "seq_value")
    private long depId;

    // ...
}

В этом примере мы видим, что другие атрибуты, такие какpkColumnName иvalueColumnName, также можно настроить.

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

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

3.5. Пользовательский генератор

Если мы не хотим использовать какие-либо готовые стратегии,we can define our custom generator by implementing the IdentifierGenerator interface.

Давайте создадим генератор, который строит идентификаторы, содержащие префиксString и число:

public class MyGenerator
  implements IdentifierGenerator, Configurable {

    private String prefix;

    @Override
    public Serializable generate(
      SharedSessionContractImplementor session, Object obj)
      throws HibernateException {

        String query = String.format("select %s from %s",
            session.getEntityPersister(obj.getClass().getName(), obj)
              .getIdentifierPropertyName(),
            obj.getClass().getSimpleName());

        Stream ids = session.createQuery(query).stream();

        Long max = ids.map(o -> o.replace(prefix + "-", ""))
          .mapToLong(Long::parseLong)
          .max()
          .orElse(0L);

        return prefix + "-" + (max + 1);
    }

    @Override
    public void configure(Type type, Properties properties,
      ServiceRegistry serviceRegistry) throws MappingException {
        prefix = properties.getProperty("prefix");
    }
}

В этом примереwe override the generate() method from the IdentifierGenerator interface и сначала найдите наибольшее число из существующих первичных ключей формыprefix-XX.

Затем мы добавляем 1 к максимальному найденному числу и добавляем свойствоprefix, чтобы получить вновь сгенерированное значение id.

Наш класс также реализует интерфейсConfigurable, поэтому мы можем установить значение свойстваprefix в методеconfigure().

Затем давайте добавим этот настраиваемый генератор к сущности. Для этогоwe can use the @GenericGenerator annotation with a strategy parameter that contains the full class name of our generator class:

@Entity
public class Product {

    @Id
    @GeneratedValue(generator = "prod-generator")
    @GenericGenerator(name = "prod-generator",
      parameters = @Parameter(name = "prefix", value = "prod"),
      strategy = "com.example.hibernate.pojo.generator.MyGenerator")
    private String prodId;

    // ...
}

Также обратите внимание, что мы установили для параметра префикса значение «prod».

Давайте посмотрим на быстрый тест JUnit для более четкого понимания сгенерированных значений id:

@Test
public void whenSaveCustomGeneratedId_thenOk() {
    Product product = new Product();
    session.save(product);
    Product product2 = new Product();
    session.save(product2);

    assertThat(product2.getProdId()).isEqualTo("prod-2");
}

Здесь первое значение, сгенерированное с использованием префикса «prod», было «prod-1», за которым следует «prod-2».

4. Составные идентификаторы

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

Составной идентификатор представлен классом первичного ключа с одним или несколькими постоянными атрибутами.

Класс первичного ключа должен удовлетворять нескольким условиям:

  • он должен быть определен с использованием аннотаций@EmbeddedId или@IdClass

  • он должен быть публичным, сериализуемым и иметь общедоступный конструктор без аргументов

  • он должен реализовывать методыequals() иhashCode()

Атрибуты класса могут быть базовыми, составными или ManyToOne, избегая при этом коллекций и атрибутовOneToOne.

4.1. @EmbeddedIdс

Чтобы определить идентификатор с помощью@EmbeddedId,, нам понадобится класс первичного ключа, аннотированный@Embeddable:

@Embeddable
public class OrderEntryPK implements Serializable {

    private long orderId;
    private long productId;

    // standard constructor, getters, setters
    // equals() and hashCode()
}

Затем мы можем добавить к сущности идентификатор типаOrderEntryPK, используя @EmbeddedId:

@Entity
public class OrderEntry {

    @EmbeddedId
    private OrderEntryPK entryId;

    // ...
}

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

@Test
public void whenSaveCompositeIdEntity_thenOk() {
    OrderEntryPK entryPK = new OrderEntryPK();
    entryPK.setOrderId(1L);
    entryPK.setProductId(30L);

    OrderEntry entry = new OrderEntry();
    entry.setEntryId(entryPK);
    session.save(entry);

    assertThat(entry.getEntryId().getOrderId()).isEqualTo(1L);
}

Здесь объектOrderEntry имеет первичный идентификаторOrderEntryPK, состоящий из двух атрибутов:orderId иproductId.

4.2. @IdClassс

Аннотация@IdClass аналогична@EmbeddedId,, за исключением того, что атрибуты определены в основном классе сущности с использованием@Id для каждого из них.

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

Давайте перепишем примерOrderEntry с@IdClass:

@Entity
@IdClass(OrderEntryPK.class)
public class OrderEntry {
    @Id
    private long orderId;
    @Id
    private long productId;

    // ...
}

Затем мы можем установить значения id непосредственно в объектеOrderEntry:

@Test
public void whenSaveIdClassEntity_thenOk() {
    OrderEntry entry = new OrderEntry();
    entry.setOrderId(1L);
    entry.setProductId(30L);
    session.save(entry);

    assertThat(entry.getOrderId()).isEqualTo(1L);
}

Обратите внимание, что для обоих типов составных идентификаторов класс первичного ключа также может содержать атрибуты@ManyToOne.

Hibernate также позволяет определять первичные ключи, состоящие из ассоциаций@ManyToOne в сочетании с аннотацией@Id. В этом случае класс сущности также должен удовлетворять условиям класса первичного ключа.

Недостатком этого метода является отсутствие разделения между объектом сущности и идентификатором.

5. Производные идентификаторы

Производные идентификаторы получаются из ассоциации объекта с использованием аннотации@MapsId.

Во-первых, давайте создадим объектUserProfile, который получает свой идентификатор из однозначной ассоциации с объектомUser:

@Entity
public class UserProfile {

    @Id
    private long profileId;

    @OneToOne
    @MapsId
    private User user;

    // ...
}

Затем давайте проверим, что экземплярUserProfile имеет тот же идентификатор, что и связанный с ним экземплярUser:

@Test
public void whenSaveDerivedIdEntity_thenOk() {
    User user = new User();
    session.save(user);

    UserProfile profile = new UserProfile();
    profile.setUser(user);
    session.save(profile);

    assertThat(profile.getProfileId()).isEqualTo(user.getUserId());
}

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

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

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

Related