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

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

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

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. Сгенерированные идентификаторы

Если мы хотим автоматически сгенерировать значение первичного ключа, мы можем добавить аннотацию @GeneratedValue .

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

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

3.1. АВТО Генерация

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

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

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

@Entity
public class Student {

@Id
@GeneratedValue
private long studentId;

// ...
}

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

Теперь мы рассмотрим UUIDGenerator , который был представлен в Hibernate 5.

Чтобы использовать эту функцию, нам просто нужно объявить идентификатор типа UUID с аннотацией @GeneratedValue :

@Entity
public class Course {

@Id
@GeneratedValue
private UUID courseId;

// ...
}

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

3.2. Генерация ИДЕНТИЧНОСТИ

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

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

@Entity
public class Student {

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

// ...
}

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

3.3. ПОСЛЕДОВАТЕЛЬНОСТЬ Генерация

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

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

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

@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.

Сгенерированные значения уникальны для каждой последовательности. Если мы не укажем имя последовательности, 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. Пользовательский генератор

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

Мы создадим генератор, который строит идентификаторы, содержащие префикс 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");
}
}

В этом примере мы переопределяем метод generate() из интерфейса IdentifierGenerator .

Во-первых, мы хотим найти наибольшее число из существующих первичных ключей формы префикс-XX . Затем мы добавляем 1 к максимальному найденному числу и добавляем свойство префикса , чтобы получить вновь сгенерированное значение идентификатора.

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

Далее давайте добавим этот пользовательский генератор к сущности.

Для этого мы можем использовать аннотацию @GenericGenerator с параметром стратегии , который содержит полное имя класса нашего класса-генератора :

@Entity
public class Product {

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

// ...
}

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

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

@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 . Разница с @IdClass заключается в том, что атрибуты определяются в основном классе сущностей с использованием @Id для каждого из них. Класс первичного ключа будет выглядеть так же, как и раньше.

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

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

// ...
}

Затем мы можем установить значения идентификатора непосредственно в объекте 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.

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